From 107bd6b20f312dca5046c89e0f86bfd5a1c37eeb Mon Sep 17 00:00:00 2001 From: "gayoun.park" Date: Thu, 20 Nov 2025 14:36:59 +0900 Subject: [PATCH] =?UTF-8?q?API=20=EB=A1=9C=EA=B7=B8=EC=A0=80=EC=9E=A5,=20E?= =?UTF-8?q?xceptionHandler=20=EC=A0=80=EC=9E=A5,=20=EA=B0=90=EC=82=AC,?= =?UTF-8?q?=EC=97=90=EB=9F=AC=EB=A1=9C=EA=B7=B8API=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/GlobalExceptionHandler.java | 163 ++++++- .../cd/kamcoback/config/api/ApiLogFilter.java | 29 ++ .../kamcoback/config/api/ApiLogFunction.java | 111 +++++ .../config/api/ApiResponseAdvice.java | 29 ++ .../kamcoback/config/api/ApiResponseDto.java | 32 +- .../kamcoback/log/AuditLogApiController.java | 127 ++++++ .../kamcoback/log/ErrorLogApiController.java | 47 ++ .../cd/kamcoback/log/dto/AuditLogDto.java | 181 ++++++++ .../cd/kamcoback/log/dto/ErrorLogDto.java | 121 +++++ .../cd/kamcoback/log/dto/EventStatus.java | 20 + .../kamco/cd/kamcoback/log/dto/EventType.java | 25 + .../postgres/core/AuditLogCoreService.java | 58 +++ .../postgres/core/ErrorLogCoreService.java | 38 ++ .../postgres/entity/AuditLogEntity.java | 85 ++++ .../postgres/entity/ErrorLogEntity.java | 50 ++ .../kamcoback/postgres/entity/MenuEntity.java | 53 +++ .../kamcoback/postgres/entity/UserEntity.java | 41 ++ .../repository/log/AuditLogRepository.java | 6 + .../log/AuditLogRepositoryCustom.java | 21 + .../log/AuditLogRepositoryImpl.java | 427 ++++++++++++++++++ .../repository/log/ErrorLogRepository.java | 6 + .../log/ErrorLogRepositoryCustom.java | 12 + .../log/ErrorLogRepositoryImpl.java | 117 +++++ 23 files changed, 1791 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/kamco/cd/kamcoback/config/api/ApiLogFilter.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/config/api/ApiLogFunction.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/log/AuditLogApiController.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/log/ErrorLogApiController.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/log/dto/AuditLogDto.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/log/dto/ErrorLogDto.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/log/dto/EventStatus.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/log/dto/EventType.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/postgres/core/AuditLogCoreService.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/postgres/core/ErrorLogCoreService.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/postgres/entity/AuditLogEntity.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/postgres/entity/ErrorLogEntity.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/postgres/entity/MenuEntity.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/postgres/entity/UserEntity.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/AuditLogRepository.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/AuditLogRepositoryCustom.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/AuditLogRepositoryImpl.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/ErrorLogRepository.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/ErrorLogRepositoryCustom.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/ErrorLogRepositoryImpl.java diff --git a/src/main/java/com/kamco/cd/kamcoback/config/GlobalExceptionHandler.java b/src/main/java/com/kamco/cd/kamcoback/config/GlobalExceptionHandler.java index b097f56b..b3809687 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/GlobalExceptionHandler.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/GlobalExceptionHandler.java @@ -1,24 +1,177 @@ package com.kamco.cd.kamcoback.config; +import com.kamco.cd.kamcoback.config.api.ApiLogFunction; import com.kamco.cd.kamcoback.config.api.ApiResponseDto; +import com.kamco.cd.kamcoback.config.api.ApiResponseDto.ApiResponseCode; +import com.kamco.cd.kamcoback.log.dto.ErrorLogDto; +import com.kamco.cd.kamcoback.postgres.entity.ErrorLogEntity; +import com.kamco.cd.kamcoback.postgres.repository.log.ErrorLogRepository; import jakarta.persistence.EntityNotFoundException; +import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.core.annotation.Order; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.client.HttpServerErrorException; + +import java.nio.file.AccessDeniedException; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.stream.Collectors; @Slf4j @Order(value = 1) @RestControllerAdvice public class GlobalExceptionHandler { - // 로그인 정보가 잘못됐습니다 권한 없음 - @org.springframework.web.bind.annotation.ResponseStatus(HttpStatus.NOT_FOUND) + private final ErrorLogRepository errorLogRepository; + + public GlobalExceptionHandler(ErrorLogRepository errorLogRepository) { + this.errorLogRepository = errorLogRepository; + } + + @ResponseStatus(HttpStatus.NOT_FOUND) @ExceptionHandler(EntityNotFoundException.class) - public ApiResponseDto handlerEntityNotFoundException(EntityNotFoundException e) { + public ApiResponseDto handlerEntityNotFoundException(EntityNotFoundException e, HttpServletRequest request) { log.warn("[EntityNotFoundException] resource :{} ", e.getMessage()); - String message = String.format("%s [%s]", e.getMessage(), e.getCause()); - return ApiResponseDto.createException(ApiResponseDto.ApiResponseCode.NOT_FOUND, message); + String codeName = "NOT_FOUND"; + ErrorLogEntity errorLog = saveErrerLogData(request, ApiResponseCode.getCode(codeName), + HttpStatus.valueOf(codeName), ErrorLogDto.LogErrorLevel.ERROR, e.getStackTrace()); + + return ApiResponseDto.createException(ApiResponseCode.getCode(codeName), ApiResponseCode.getMessage(codeName), HttpStatus.valueOf(codeName), errorLog.getId()); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(HttpMessageNotReadableException.class) + public ApiResponseDto handleUnreadable(HttpMessageNotReadableException e, HttpServletRequest request) { + log.warn("[HttpMessageNotReadableException] resource :{} ", e.getMessage()); + String codeName = "BAD_REQUEST"; + ErrorLogEntity errorLog = saveErrerLogData(request, ApiResponseCode.getCode(codeName), + HttpStatus.valueOf(codeName), ErrorLogDto.LogErrorLevel.WARNING, e.getStackTrace()); + + return ApiResponseDto.createException(ApiResponseCode.getCode(codeName), ApiResponseCode.getMessage(codeName), HttpStatus.valueOf(codeName), errorLog.getId()); + } + + @ResponseStatus(HttpStatus.NOT_FOUND) + @ExceptionHandler(NoSuchElementException.class) + public ApiResponseDto handlerNoSuchElementException(NoSuchElementException e, HttpServletRequest request) { + log.warn("[NoSuchElementException] resource :{} ", e.getMessage()); + String codeName = "NOT_FOUND_DATA"; + ErrorLogEntity errorLog = saveErrerLogData(request, ApiResponseCode.getCode(codeName), + HttpStatus.valueOf(codeName), ErrorLogDto.LogErrorLevel.WARNING, e.getStackTrace()); + + return ApiResponseDto.createException(ApiResponseCode.getCode(codeName), ApiResponseCode.getMessage(codeName), HttpStatus.valueOf("NOT_FOUND"), errorLog.getId()); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(IllegalArgumentException.class) + public ApiResponseDto handlerIllegalArgumentException(IllegalArgumentException e, HttpServletRequest request) { + log.warn("[handlerIllegalArgumentException] resource :{} ", e.getMessage()); + String codeName = "BAD_REQUEST"; + ErrorLogEntity errorLog = saveErrerLogData(request, ApiResponseCode.getCode(codeName), + HttpStatus.valueOf(codeName), ErrorLogDto.LogErrorLevel.WARNING, e.getStackTrace()); + + return ApiResponseDto.createException(ApiResponseCode.getCode(codeName), ApiResponseCode.getMessage(codeName), HttpStatus.valueOf(codeName), errorLog.getId()); + } + + @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) + @ExceptionHandler(DataIntegrityViolationException.class) + public ApiResponseDto handlerDataIntegrityViolationException(DataIntegrityViolationException e, HttpServletRequest request) { + log.warn("[DataIntegrityViolationException] resource :{} ", e.getMessage()); + String codeName = "DATA_INTEGRITY_ERROR"; + ErrorLogEntity errorLog = saveErrerLogData(request, ApiResponseCode.getCode(codeName), + HttpStatus.valueOf(codeName), ErrorLogDto.LogErrorLevel.CRITICAL, e.getStackTrace()); + + return ApiResponseDto.createException(ApiResponseCode.getCode(codeName), ApiResponseCode.getMessage(codeName), HttpStatus.valueOf("UNPROCESSABLE_ENTITY"), errorLog.getId()); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MethodArgumentNotValidException.class) + public ApiResponseDto handlerMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) { + log.warn("[MethodArgumentNotValidException] resource :{} ", e.getMessage()); + String codeName = "BAD_REQUEST"; + ErrorLogEntity errorLog = saveErrerLogData(request, ApiResponseCode.getCode(codeName), + HttpStatus.valueOf(codeName), ErrorLogDto.LogErrorLevel.WARNING, e.getStackTrace()); + + return ApiResponseDto.createException(ApiResponseCode.getCode(codeName), ApiResponseCode.getMessage(codeName), HttpStatus.valueOf(codeName), errorLog.getId()); + } + + @ResponseStatus(HttpStatus.UNAUTHORIZED) + @ExceptionHandler(AccessDeniedException.class) + public ApiResponseDto handlerAccessDeniedException(AccessDeniedException e, HttpServletRequest request) { + log.warn("[AccessDeniedException] resource :{} ", e.getMessage()); + String codeName = "UNAUTHORIZED"; + ErrorLogEntity errorLog = saveErrerLogData(request, ApiResponseCode.getCode(codeName), + HttpStatus.valueOf(codeName), ErrorLogDto.LogErrorLevel.ERROR, e.getStackTrace()); + + return ApiResponseDto.createException(ApiResponseCode.getCode(codeName), ApiResponseCode.getMessage(codeName), HttpStatus.valueOf(codeName), errorLog.getId()); + } + + @ResponseStatus(HttpStatus.BAD_GATEWAY) + @ExceptionHandler(HttpServerErrorException.BadGateway.class) + public ApiResponseDto handlerHttpServerErrorException(HttpServerErrorException e, HttpServletRequest request) { + log.warn("[HttpServerErrorException] resource :{} ", e.getMessage()); + String codeName = "BAD_GATEWAY"; + ErrorLogEntity errorLog = saveErrerLogData(request, ApiResponseCode.getCode(codeName), + HttpStatus.valueOf(codeName), ErrorLogDto.LogErrorLevel.CRITICAL, e.getStackTrace()); + + return ApiResponseDto.createException(ApiResponseCode.getCode(codeName), ApiResponseCode.getMessage(codeName), HttpStatus.valueOf(codeName), errorLog.getId()); + } + + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(RuntimeException.class) + public ApiResponseDto handlerRuntimeException(RuntimeException e, HttpServletRequest request) { + log.warn("[RuntimeException] resource :{} ", e.getMessage()); + + String codeName = "INTERNAL_SERVER_ERROR"; + ErrorLogEntity errorLog = saveErrerLogData(request, ApiResponseCode.getCode(codeName), + HttpStatus.valueOf(codeName), ErrorLogDto.LogErrorLevel.CRITICAL, e.getStackTrace()); + + return ApiResponseDto.createException(ApiResponseCode.getCode(codeName), ApiResponseCode.getMessage(codeName), HttpStatus.valueOf(codeName), errorLog.getId()); + } + + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(Exception.class) + public ApiResponseDto handlerException(Exception e, HttpServletRequest request) { + log.warn("[Exception] resource :{} ", e.getMessage()); + + String codeName = "INTERNAL_SERVER_ERROR"; + ErrorLogEntity errorLog = saveErrerLogData(request, ApiResponseCode.getCode(codeName), + HttpStatus.valueOf(codeName), ErrorLogDto.LogErrorLevel.CRITICAL, e.getStackTrace()); + + return ApiResponseDto.createException(ApiResponseCode.getCode(codeName), ApiResponseCode.getMessage(codeName), HttpStatus.valueOf(codeName), errorLog.getId()); + } + + /** + * 에러 로그 테이블 저장 로직 + * @param request : request + * @param errorCode : 정의된 enum errorCode + * @param httpStatus : HttpStatus 값 + * @param logErrorLevel : WARNING, ERROR, CRITICAL + * @param stackTrace : 에러 내용 + * @return : insert하고 결과로 받은 Entity + */ + private ErrorLogEntity saveErrerLogData(HttpServletRequest request, ApiResponseCode errorCode, + HttpStatus httpStatus, ErrorLogDto.LogErrorLevel logErrorLevel, StackTraceElement[] stackTrace) { + //TODO : 로그인 개발되면 이것도 연결해야 함 + Long userid = Long.valueOf(Optional.ofNullable(ApiLogFunction.getUserId(request)).orElse("1")); + + //TODO : stackTrace limit 10줄? 확인 필요 + String stackTraceStr = Arrays.stream(stackTrace) + .limit(10) + .map(StackTraceElement::toString) + .collect(Collectors.joining("\n")); + + ErrorLogEntity errorLogEntity = new ErrorLogEntity(request.getRequestURI(), ApiLogFunction.getEventType(request), logErrorLevel, + String.valueOf(httpStatus.value()), errorCode.getText(), stackTraceStr, userid, ZonedDateTime.now()); + + return errorLogRepository.save(errorLogEntity); } } diff --git a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiLogFilter.java b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiLogFilter.java new file mode 100644 index 00000000..f678cfb4 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiLogFilter.java @@ -0,0 +1,29 @@ +package com.kamco.cd.kamcoback.config.api; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import java.io.IOException; + +@Component +public class ApiLogFilter extends OncePerRequestFilter { + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + ContentCachingRequestWrapper wrappedRequest = + new ContentCachingRequestWrapper(request); + + ContentCachingResponseWrapper wrappedResponse = + new ContentCachingResponseWrapper(response); + + filterChain.doFilter(wrappedRequest, wrappedResponse); + + // 반드시 response body copy + wrappedResponse.copyBodyToResponse(); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiLogFunction.java b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiLogFunction.java new file mode 100644 index 00000000..4e9581e2 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiLogFunction.java @@ -0,0 +1,111 @@ +package com.kamco.cd.kamcoback.config.api; + +import com.kamco.cd.kamcoback.log.dto.EventStatus; +import com.kamco.cd.kamcoback.log.dto.EventType; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.util.ContentCachingRequestWrapper; + +import java.io.UnsupportedEncodingException; +import java.util.Map; +import java.util.stream.Collectors; + +public class ApiLogFunction { + // 클라이언트 IP 추출 + public static String getClientIp(HttpServletRequest request) { + String[] headers = { + "X-Forwarded-For", + "Proxy-Client-IP", + "WL-Proxy-Client-IP", + "HTTP_CLIENT_IP", + "HTTP_X_FORWARDED_FOR" + }; + for (String header : headers) { + String ip = request.getHeader(header); + if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) { + return ip.split(",")[0]; + } + } + String ip = request.getRemoteAddr(); + if ("0:0:0:0:0:0:0:1".equals(ip)) { //local 일 때 + ip = "127.0.0.1"; + } + return ip; + } + + // 사용자 ID 추출 예시 (Spring Security 기준) + public static String getUserId(HttpServletRequest request) { + try { + return request.getUserPrincipal() != null ? request.getUserPrincipal().getName() : null; + } catch (Exception e) { + return null; + } + } + + public static EventType getEventType(HttpServletRequest request) { + String method = request.getMethod().toUpperCase(); + String uri = request.getRequestURI().toLowerCase(); + + //URL 기반 DOWNLOAD/PRINT 분류 + if(uri.contains("/download") || uri.contains("/export")) return EventType.DOWNLOAD; + if(uri.contains("/print")) return EventType.PRINT; + + // 일반 CRUD + return switch (method) { + case "POST" -> EventType.CREATE; + case "GET" -> EventType.READ; + case "DELETE" -> EventType.DELETE; + case "PUT", "PATCH" -> EventType.UPDATE; + default -> EventType.OTHER; + }; + } + + public static String getRequestBody(HttpServletRequest servletRequest, ContentCachingRequestWrapper contentWrapper) { + StringBuilder resultBody = new StringBuilder(); + // GET, form-urlencoded POST 파라미터 + Map paramMap = servletRequest.getParameterMap(); + + String queryParams = paramMap.entrySet().stream() + .map(e -> e.getKey() + "=" + String.join(",", e.getValue())) + .collect(Collectors.joining("&")); + + resultBody.append(queryParams.isEmpty() ? "" : queryParams); + + // JSON Body + if ("POST".equalsIgnoreCase(servletRequest.getMethod()) + && servletRequest.getContentType() != null + && servletRequest.getContentType().contains("application/json")) { + try { + //json인 경우는 Wrapper를 통해 가져오기 + resultBody.append(getBodyData(contentWrapper)); + + } catch (Exception e) { + resultBody.append("cannot read JSON body ").append(e.toString()); + } + } + + // Multipart form-data + if ("POST".equalsIgnoreCase(servletRequest.getMethod()) + && servletRequest.getContentType() != null + && servletRequest.getContentType().startsWith("multipart/form-data")) { + resultBody.append("multipart/form-data request"); + } + + return resultBody.toString(); + } + + // JSON Body 읽기 + public static String getBodyData(ContentCachingRequestWrapper request) { + byte[] buf = request.getContentAsByteArray(); + if (buf.length == 0) return null; + try { + return new String(buf, request.getCharacterEncoding()); + } catch (UnsupportedEncodingException e) { + return new String(buf); + } + } + + //ApiResponse 의 Status가 2xx 범위이면 SUCCESS, 아니면 FAILED + public static EventStatus isSuccessFail(ApiResponseDto apiResponse){ + return apiResponse.getHttpStatus().is2xxSuccessful() ? EventStatus.SUCCESS : EventStatus.FAILED; + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseAdvice.java b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseAdvice.java index eb8cc764..3cc2fdef 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseAdvice.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseAdvice.java @@ -1,12 +1,19 @@ package com.kamco.cd.kamcoback.config.api; +import com.kamco.cd.kamcoback.postgres.entity.AuditLogEntity; +import com.kamco.cd.kamcoback.postgres.repository.log.AuditLogRepository; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; +import org.springframework.web.util.ContentCachingRequestWrapper; + +import java.util.Optional; /** * ApiResponseDto의 내장된 HTTP 상태 코드를 실제 HTTP 응답에 적용하는 Advice @@ -16,6 +23,12 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; @RestControllerAdvice public class ApiResponseAdvice implements ResponseBodyAdvice { + private final AuditLogRepository auditLogRepository; + + public ApiResponseAdvice(AuditLogRepository auditLogRepository) { + this.auditLogRepository = auditLogRepository; + } + @Override public boolean supports( MethodParameter returnType, Class> converterType) { @@ -32,9 +45,25 @@ public class ApiResponseAdvice implements ResponseBodyAdvice { ServerHttpRequest request, ServerHttpResponse response) { + HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest(); + ContentCachingRequestWrapper contentWrapper = (ContentCachingRequestWrapper) servletRequest; + if (body instanceof ApiResponseDto apiResponse) { // ApiResponseDto에 설정된 httpStatus를 실제 HTTP 응답에 적용 response.setStatusCode(apiResponse.getHttpStatus()); + + String ip = ApiLogFunction.getClientIp(servletRequest); + //TODO : userid 가 계정명인지, uid 인지 확인 후 로직 수정 필요함 + Long userid = Long.valueOf(Optional.ofNullable(ApiLogFunction.getUserId(servletRequest)).orElse("1")); + + //TODO: menuUid 를 동적으로 가져오게끔 해야함 + AuditLogEntity log = new AuditLogEntity(userid, ApiLogFunction.getEventType(servletRequest), + ApiLogFunction.isSuccessFail(apiResponse), "MU_01_01", ip, servletRequest.getRequestURI(), ApiLogFunction.getRequestBody(servletRequest, contentWrapper), + apiResponse.getErrorLogUid() + ); + + //tb_audit_log 테이블 저장 + auditLogRepository.save(log); } return body; diff --git a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java index 2146a543..287b9114 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java @@ -20,7 +20,9 @@ public class ApiResponseDto { @JsonInclude(JsonInclude.Include.NON_NULL) private T errorData; - @JsonIgnore private HttpStatus httpStatus = HttpStatus.OK; + @JsonIgnore private HttpStatus httpStatus; + + @JsonIgnore private Long errorLogUid; public ApiResponseDto(T data) { this.data = data; @@ -38,6 +40,15 @@ public class ApiResponseDto { public ApiResponseDto(ApiResponseCode code, String message) { this.error = new Error(code.getId(), message); } + public ApiResponseDto(ApiResponseCode code, String message, HttpStatus httpStatus) { + this.error = new Error(code.getId(), message); + this.httpStatus = httpStatus; + } + public ApiResponseDto(ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) { + this.error = new Error(code.getId(), message); + this.httpStatus = httpStatus; + this.errorLogUid = errorLogUid; + } public ApiResponseDto(ApiResponseCode code, String message, T errorData) { this.error = new Error(code.getId(), message); @@ -64,6 +75,12 @@ public class ApiResponseDto { public static ApiResponseDto createException(ApiResponseCode code, String message) { return new ApiResponseDto<>(code, message); } + public static ApiResponseDto createException(ApiResponseCode code, String message, HttpStatus httpStatus) { + return new ApiResponseDto<>(code, message, httpStatus); + } + public static ApiResponseDto createException(ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) { + return new ApiResponseDto<>(code, message, httpStatus, errorLogUid); + } public static ApiResponseDto createException( ApiResponseCode code, String message, T data) { @@ -89,6 +106,7 @@ public class ApiResponseDto { // @formatter:off OK("요청이 성공하였습니다."), BAD_REQUEST("요청 파라미터가 잘못되었습니다."), + BAD_GATEWAY("네트워크 상태가 불안정합니다."), ALREADY_EXIST_MALL("이미 등록된 쇼핑센터입니다."), NOT_FOUND_MAP("지도를 찾을 수 없습니다."), UNAUTHORIZED("권한이 없습니다."), @@ -111,8 +129,8 @@ public class ApiResponseDto { REQUIRED_EMAIL("이메일은 필수 항목입니다."), WRONG_PASSWORD("잘못된 패스워드입니다.."), DUPLICATE_EMAIL("이미 가입된 이메일입니다."), - DUPLICATE_DATA("이미 등록되여 있습니다."), - DATA_INTEGRITY_ERROR("요청을 처리할수 없습니다."), + DUPLICATE_DATA("이미 등록되어 있습니다."), + DATA_INTEGRITY_ERROR("데이터 무결성이 위반되어 요청을 처리할수 없습니다."), FOREIGN_KEY_ERROR("참조 중인 데이터가 있어 삭제할 수 없습니다."), DUPLICATE_EMPLOYEEID("이미 가입된 사번입니다."), NOT_FOUND_USER_FOR_EMAIL("이메일로 유저를 찾을 수 없습니다."), @@ -134,5 +152,13 @@ public class ApiResponseDto { public String getText() { return message; } + + public static ApiResponseCode getCode(String name) { + return ApiResponseCode.valueOf(name.toUpperCase()); + } + + public static String getMessage(String name) { + return ApiResponseCode.valueOf(name.toUpperCase()).getText(); + } } } diff --git a/src/main/java/com/kamco/cd/kamcoback/log/AuditLogApiController.java b/src/main/java/com/kamco/cd/kamcoback/log/AuditLogApiController.java new file mode 100644 index 00000000..cbf8c3fe --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/log/AuditLogApiController.java @@ -0,0 +1,127 @@ +package com.kamco.cd.kamcoback.log; + +import com.kamco.cd.kamcoback.config.api.ApiResponseDto; +import com.kamco.cd.kamcoback.log.dto.AuditLogDto; +import com.kamco.cd.kamcoback.postgres.core.AuditLogCoreService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +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; + +import java.time.LocalDate; + +@Tag(name = "감사 로그", description = "감사 로그 관리 API") +@RequiredArgsConstructor +@RestController +@RequestMapping({"/api/log/audit", "/v1/api/log/audit"}) +public class AuditLogApiController { + + private final AuditLogCoreService auditLogCoreService; + + @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.DailySearchReq searchReq = new AuditLogDto.DailySearchReq(startDate, endDate, null, page, size, "created_dttm,desc"); + + Page result = auditLogCoreService.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.DailySearchReq searchReq = new AuditLogDto.DailySearchReq(null, null, logDate, page, size, "created_dttm,desc"); + Page result = auditLogCoreService.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.MenuUserSearchReq searchReq = new AuditLogDto.MenuUserSearchReq(searchValue, null, null, page, size, "created_dttm,desc"); + Page result = auditLogCoreService.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.MenuUserSearchReq searchReq = new AuditLogDto.MenuUserSearchReq(null, menuId, null, page, size, "created_dttm,desc"); + Page result = auditLogCoreService.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.MenuUserSearchReq searchReq = new AuditLogDto.MenuUserSearchReq(searchValue, null, null, page, size, "created_dttm,desc"); + Page result = auditLogCoreService.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.MenuUserSearchReq searchReq = new AuditLogDto.MenuUserSearchReq(null, null, userUid, page, size, "created_dttm,desc"); + Page result = auditLogCoreService.getLogByAccountResult( + searchReq, + userUid + ); + + return ApiResponseDto.ok(result); + } + +} diff --git a/src/main/java/com/kamco/cd/kamcoback/log/ErrorLogApiController.java b/src/main/java/com/kamco/cd/kamcoback/log/ErrorLogApiController.java new file mode 100644 index 00000000..1a7d2973 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/log/ErrorLogApiController.java @@ -0,0 +1,47 @@ +package com.kamco.cd.kamcoback.log; + +import com.kamco.cd.kamcoback.config.api.ApiResponseDto; +import com.kamco.cd.kamcoback.log.dto.AuditLogDto; +import com.kamco.cd.kamcoback.log.dto.ErrorLogDto; +import com.kamco.cd.kamcoback.log.dto.EventType; +import com.kamco.cd.kamcoback.postgres.core.AuditLogCoreService; +import com.kamco.cd.kamcoback.postgres.core.ErrorLogCoreService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +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; + +import java.time.LocalDate; +import java.util.List; + +@Tag(name = "에러 로그", description = "에러 로그 관리 API") +@RequiredArgsConstructor +@RestController +@RequestMapping({"/api/log/error", "/v1/api/log/error"}) +public class ErrorLogApiController { + + private final ErrorLogCoreService errorLogCoreService; + + @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 = errorLogCoreService.findLogByError(searchReq); + + return ApiResponseDto.ok(result); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/log/dto/AuditLogDto.java b/src/main/java/com/kamco/cd/kamcoback/log/dto/AuditLogDto.java new file mode 100644 index 00000000..d4dc30ac --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/log/dto/AuditLogDto.java @@ -0,0 +1,181 @@ +package com.kamco.cd.kamcoback.log.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.kamco.cd.kamcoback.common.utils.interfaces.JsonFormatDttm; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; + +public class AuditLogDto { + + @Schema(name = "AuditLogBasic", description = "감사로그 기본 정보") + @Getter + public static class Basic { + + @JsonIgnore + private final Long id; + private final Long userUid; + private final EventType eventType; + private final EventStatus eventStatus; + private final String menuUid; + private final String ipAddress; + private final String requestUri; + private final String requestBody; + private final Long errorLogUid; + + @JsonFormatDttm + private final ZonedDateTime createdDttm; + + public Basic( + Long id, + Long userUid, + EventType eventType, + EventStatus eventStatus, + String menuUid, + String ipAddress, + String requestUri, + String requestBody, + Long errorLogUid, + ZonedDateTime createdDttm) { + this.id = id; + this.userUid = userUid; + this.eventType = eventType; + this.eventStatus = eventStatus; + this.menuUid = menuUid; + this.ipAddress = ipAddress; + this.requestUri = requestUri; + this.requestBody = requestBody; + this.errorLogUid = errorLogUid; + this.createdDttm = createdDttm; + } + } + + @Schema(name = "AuditList", description = "감사 로그 목록") + @Getter + @AllArgsConstructor + public static class AuditList { + private int readCount; + private int cudCount; + private int printCount; + private int downloadCount; + private Long totalCount; + + private Long accountId; + private String loginId; + private String username; + private LocalDateTime baseDate; + private Long menuId; + private String menuName; + + public AuditList(LocalDateTime baseDate, int readCount, int cudCount, int printCount, int downloadCount, Long totalCount){ + this.baseDate = baseDate; + this.readCount = readCount; + this.cudCount = cudCount; + this.printCount = printCount; + this.downloadCount = downloadCount; + this.totalCount = totalCount; + } + } + + @Schema(name = "AuditDetail", description = "감사 로그 상세") + @Getter + @AllArgsConstructor + public static class AuditDetail { + private Long logId; + private LocalDateTime logDateTime; + private EventType eventType; + private LogDetail detail; + + private String userName; + private String loginId; + private String menuName; + } + + @Getter + @Setter + @AllArgsConstructor + public static class LogDetail{ + String serviceName; + String parentMenuName; + String menuName; + String menuUrl; + String menuDescription; + int sortOrder; + boolean used; + } + + @Schema(name = "LogDailySearchReq", description = "일자별 로그 검색 요청") + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class DailySearchReq { + + private LocalDate startDate; + private LocalDate endDate; + + // 일자별 로그 검색 조건 + private LocalDate logDate; + + // 페이징 파라미터 + private int page = 0; + private int size = 20; + private String sort; + + public Pageable toPageable() { + if (sort != null && !sort.isEmpty()) { + String[] sortParams = sort.split(","); + String property = sortParams[0]; + Sort.Direction direction = + sortParams.length > 1 ? Sort.Direction.fromString(sortParams[1]) : Sort.Direction.ASC; + return PageRequest.of(page, size, Sort.by(direction, property)); + } + return PageRequest.of(page, size); + } + } + + @Schema(name = "MenuUserSearchReq", description = "메뉴별,사용자별 로그 검색 요청") + @Getter + @Setter + @NoArgsConstructor + public static class MenuUserSearchReq { + + // 메뉴별, 사용자별 로그 검색 조건 + private String searchValue; + private String menuUid; + private Long userUid; //menuId, userUid 조회 + + // 페이징 파라미터 + private int page = 0; + private int size = 20; + private String sort; + + public MenuUserSearchReq(String searchValue, String menuUid, Long userUid, int page, int size, String sort) { + this.searchValue = searchValue; + this.menuUid = menuUid; + this.page = page; + this.size = size; + this.sort = sort; + } + + public Pageable toPageable() { + if (sort != null && !sort.isEmpty()) { + String[] sortParams = sort.split(","); + String property = sortParams[0]; + Sort.Direction direction = + sortParams.length > 1 ? Sort.Direction.fromString(sortParams[1]) : Sort.Direction.ASC; + return PageRequest.of(page, size, Sort.by(direction, property)); + } + return PageRequest.of(page, size); + } + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/log/dto/ErrorLogDto.java b/src/main/java/com/kamco/cd/kamcoback/log/dto/ErrorLogDto.java new file mode 100644 index 00000000..8b50504e --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/log/dto/ErrorLogDto.java @@ -0,0 +1,121 @@ +package com.kamco.cd.kamcoback.log.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.kamco.cd.kamcoback.common.utils.interfaces.JsonFormatDttm; +import com.kamco.cd.kamcoback.config.enums.EnumType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; + +public class ErrorLogDto { + + @Schema(name = "ErrorLogBasic", description = "에러로그 기본 정보") + @Getter + public static class Basic { + + @JsonIgnore + private final Long id; + private final String requestId; + private final EventType errorType; + private final LogErrorLevel errorLevel; + private final String errorCode; + private final String errorMessage; + private final String stackTrace; + private final Long handlerUid; + + @JsonFormatDttm + private final ZonedDateTime handledDttm; + + @JsonFormatDttm + private final ZonedDateTime createdDttm; + + public Basic( + Long id, + String requestId, + EventType errorType, + LogErrorLevel errorLevel, + String errorCode, + String errorMessage, + String stackTrace, + Long handlerUid, + ZonedDateTime handledDttm, + ZonedDateTime createdDttm) { + this.id = id; + this.requestId = requestId; + this.errorType = errorType; + this.errorLevel = errorLevel; + this.errorCode = errorCode; + this.errorMessage = errorMessage; + this.stackTrace = stackTrace; + this.handlerUid = handlerUid; + this.handledDttm = handledDttm; + this.createdDttm = createdDttm; + } + } + + @Schema(name = "ErrorSearchReq", description = "에러로그 검색 요청") + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class ErrorSearchReq { + + LogErrorLevel errorLevel; + EventType eventType; + LocalDate startDate; + LocalDate endDate; + + // 페이징 파라미터 + private int page = 0; + private int size = 20; + private String sort; + + public ErrorSearchReq(LogErrorLevel errorLevel, EventType eventType, LocalDate startDate, LocalDate endDate, int page, int size) { + this.errorLevel = errorLevel; + this.eventType = eventType; + this.startDate = startDate; + this.endDate = endDate; + this.page = page; + this.size = size; + } + + public Pageable toPageable() { + if (sort != null && !sort.isEmpty()) { + String[] sortParams = sort.split(","); + String property = sortParams[0]; + Sort.Direction direction = + sortParams.length > 1 ? Sort.Direction.fromString(sortParams[1]) : Sort.Direction.ASC; + return PageRequest.of(page, size, Sort.by(direction, property)); + } + return PageRequest.of(page, size); + } + } + + public enum LogErrorLevel implements EnumType { + WARNING("Warning"), + ERROR("Error"), + CRITICAL("Critical"); + + private final String desc; + + LogErrorLevel(String desc) { + this.desc = desc; + } + + @Override + public String getId() { return name(); } + + @Override + public String getText() { return desc; } + } + +} diff --git a/src/main/java/com/kamco/cd/kamcoback/log/dto/EventStatus.java b/src/main/java/com/kamco/cd/kamcoback/log/dto/EventStatus.java new file mode 100644 index 00000000..ed50fd47 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/log/dto/EventStatus.java @@ -0,0 +1,20 @@ +package com.kamco.cd.kamcoback.log.dto; + +import com.kamco.cd.kamcoback.config.enums.EnumType; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum EventStatus implements EnumType { + SUCCESS("이벤트 결과 성공"), + FAILED("이벤트 결과 실패"); + + private final String desc; + + @Override + public String getId() { return name(); } + + @Override + public String getText() { return desc; } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/log/dto/EventType.java b/src/main/java/com/kamco/cd/kamcoback/log/dto/EventType.java new file mode 100644 index 00000000..414a3386 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/log/dto/EventType.java @@ -0,0 +1,25 @@ +package com.kamco.cd.kamcoback.log.dto; + +import com.kamco.cd.kamcoback.config.enums.EnumType; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum EventType implements EnumType { + CREATE("생성"), + READ("조회"), + UPDATE("수정"), + DELETE("삭제"), + DOWNLOAD("다운로드"), + PRINT("출력"), + OTHER("기타"); + + private final String desc; + + @Override + public String getId() { return name(); } + + @Override + public String getText() { return desc; } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/AuditLogCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/AuditLogCoreService.java new file mode 100644 index 00000000..fdcc902f --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/AuditLogCoreService.java @@ -0,0 +1,58 @@ +package com.kamco.cd.kamcoback.postgres.core; + +import com.kamco.cd.kamcoback.common.service.BaseCoreService; +import com.kamco.cd.kamcoback.log.dto.AuditLogDto; +import com.kamco.cd.kamcoback.postgres.repository.log.AuditLogRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AuditLogCoreService implements BaseCoreService { + + private final AuditLogRepository auditLogRepository; + + @Override + public void remove(Long aLong) { + + } + + @Override + public AuditLogDto.AuditList getOneById(Long aLong) { + return null; + } + + @Override + public Page search(AuditLogDto.DailySearchReq searchReq) { + return null; + } + + public Page getLogByDaily(AuditLogDto.DailySearchReq searchRange, LocalDate startDate, LocalDate endDate) { + return auditLogRepository.findLogByDaily(searchRange, startDate, endDate); + } + + public Page getLogByMenu(AuditLogDto.MenuUserSearchReq searchRange, String searchValue) { + return auditLogRepository.findLogByMenu(searchRange, searchValue); + } + + public Page getLogByAccount(AuditLogDto.MenuUserSearchReq searchRange, String searchValue) { + return auditLogRepository.findLogByAccount(searchRange, searchValue); + } + + public Page getLogByDailyResult(AuditLogDto.DailySearchReq searchRange, LocalDate logDate) { + return auditLogRepository.findLogByDailyResult(searchRange, logDate); + } + + public Page getLogByMenuResult(AuditLogDto.MenuUserSearchReq searchRange, String menuId) { + return auditLogRepository.findLogByMenuResult(searchRange, menuId); + } + + public Page getLogByAccountResult(AuditLogDto.MenuUserSearchReq searchRange, Long accountId) { + return auditLogRepository.findLogByAccountResult(searchRange, accountId); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/ErrorLogCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/ErrorLogCoreService.java new file mode 100644 index 00000000..b833f3d0 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/ErrorLogCoreService.java @@ -0,0 +1,38 @@ +package com.kamco.cd.kamcoback.postgres.core; + +import com.kamco.cd.kamcoback.common.service.BaseCoreService; +import com.kamco.cd.kamcoback.log.dto.AuditLogDto; +import com.kamco.cd.kamcoback.log.dto.ErrorLogDto; +import com.kamco.cd.kamcoback.postgres.repository.log.AuditLogRepository; +import com.kamco.cd.kamcoback.postgres.repository.log.ErrorLogRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ErrorLogCoreService implements BaseCoreService { + + private final ErrorLogRepository errorLogRepository; + + public Page findLogByError(ErrorLogDto.ErrorSearchReq searchReq) { + return errorLogRepository.findLogByError(searchReq); + } + + @Override + public void remove(Long aLong) {} + + @Override + public ErrorLogDto.Basic getOneById(Long aLong) { + return null; + } + + @Override + public Page search(ErrorLogDto.ErrorSearchReq searchReq) { + return null; + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/AuditLogEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/AuditLogEntity.java new file mode 100644 index 00000000..7570f66d --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/AuditLogEntity.java @@ -0,0 +1,85 @@ +package com.kamco.cd.kamcoback.postgres.entity; + +import com.kamco.cd.kamcoback.log.dto.AuditLogDto; +import com.kamco.cd.kamcoback.log.dto.EventStatus; +import com.kamco.cd.kamcoback.log.dto.EventType; +import com.kamco.cd.kamcoback.postgres.CommonCreateEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "tb_audit_log") +public class AuditLogEntity extends CommonCreateEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "audit_log_uid") + private Long id; + + @Column(name = "user_uid") + private Long userUid; + + @Enumerated(EnumType.STRING) + private EventType eventType; + + @Enumerated(EnumType.STRING) + private EventStatus eventStatus; + + @Column(name = "menu_uid") + private String menuUid; + + @Column(name = "ip_address") + private String ipAddress; + + @Column(name = "request_uri") + private String requestUri; + + @Column(name = "request_body") + private String requestBody; + + @Column(name = "error_log_uid") + private Long errorLogUid; + + public AuditLogEntity(Long userUid, EventType eventType, EventStatus eventStatus, String menuUid, String ipAddress, String requestUri, String requestBody, Long errorLogUid) { + this.userUid = userUid; + this.eventType = eventType; + this.eventStatus = eventStatus; + this.menuUid = menuUid; + this.ipAddress = ipAddress; + this.requestUri = requestUri; + this.requestBody = requestBody; + this.errorLogUid = errorLogUid; + } + + public AuditLogDto.Basic toDto() { + return new AuditLogDto.Basic( + this.id, + this.userUid, + this.eventType, + this.eventStatus, + this.menuUid, + this.ipAddress, + this.requestUri, + this.requestBody, + this.errorLogUid, + super.getCreatedDate()); + } + + @Override + public String toString(){ + StringBuilder sb = new StringBuilder(); + sb.append(this.id).append("\n") + .append(this.userUid).append("\n") + .append(this.eventType).append("\n") + .append(this.eventStatus).append("\n") + .append(this.menuUid).append("\n") + .append(this.ipAddress).append("\n") + .append(this.requestUri).append("\n") + .append(this.requestBody).append("\n") + .append(this.errorLogUid); + return sb.toString(); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/ErrorLogEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/ErrorLogEntity.java new file mode 100644 index 00000000..281b3d0c --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/ErrorLogEntity.java @@ -0,0 +1,50 @@ +package com.kamco.cd.kamcoback.postgres.entity; + +import com.kamco.cd.kamcoback.log.dto.ErrorLogDto; +import com.kamco.cd.kamcoback.log.dto.EventType; +import com.kamco.cd.kamcoback.postgres.CommonCreateEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.time.ZonedDateTime; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "tb_error_log") +public class ErrorLogEntity extends CommonCreateEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "error_log_uid") + private Long id; + + @Column(name = "request_id") + private String requestId; + + @Column(name = "error_type") + @Enumerated(EnumType.STRING) + private EventType errorType; + + @Enumerated(EnumType.STRING) + private ErrorLogDto.LogErrorLevel errorLevel; + private String errorCode; + private String errorMessage; + private String stackTrace; + private Long handlerUid; + private ZonedDateTime handledDttm; + + public ErrorLogEntity(String requestId, EventType errorType, ErrorLogDto.LogErrorLevel errorLevel, String errorCode, String errorMessage, String stackTrace + , Long handlerUid, ZonedDateTime handledDttm) { + this.requestId = requestId; + this.errorType = errorType; + this.errorLevel = errorLevel; + this.errorCode = errorCode; + this.errorMessage = errorMessage; + this.stackTrace = stackTrace; + this.handlerUid = handlerUid; + this.handledDttm = handledDttm; + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MenuEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MenuEntity.java new file mode 100644 index 00000000..bfb65394 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MenuEntity.java @@ -0,0 +1,53 @@ +package com.kamco.cd.kamcoback.postgres.entity; + +import com.kamco.cd.kamcoback.log.dto.EventStatus; +import com.kamco.cd.kamcoback.log.dto.EventType; +import com.kamco.cd.kamcoback.postgres.CommonDateEntity; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "tb_menu") +public class MenuEntity extends CommonDateEntity { + @Id + @Column(name = "menu_uid") + private String menuUid; + + @Column(name = "menu_nm") + private String menuNm; + + @Column(name = "menu_url") + private String menuUrl; + + @Column(name = "description") + private String description; + + @Column(name = "menu_order") + private Long menuOrder; + + @NotNull + @Column(name = "is_use", nullable = true) + private Boolean isUse = true; + + @NotNull + @Column(name = "deleted", nullable = false) + private Boolean deleted = false; + + private Long createdUid; + private Long updatedUid; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_menu_uid") + private MenuEntity parent; + + @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL) + private List children = new ArrayList<>(); +} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/UserEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/UserEntity.java new file mode 100644 index 00000000..df7631b0 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/UserEntity.java @@ -0,0 +1,41 @@ +package com.kamco.cd.kamcoback.postgres.entity; + +import com.kamco.cd.kamcoback.log.dto.ErrorLogDto; +import com.kamco.cd.kamcoback.log.dto.EventType; +import com.kamco.cd.kamcoback.postgres.CommonDateEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.ZonedDateTime; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "tb_user") +public class UserEntity extends CommonDateEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_uid") + private Long id; + + @Column(name = "user_nm") + private String userNm; + + @Column(name = "user_id") + private String userId; + + @Column(name = "pswd") + private String pswd; //TODO: 암호화 + + //@Enumerated(EnumType.STRING) + private String state; //TODO: 추후 enum -> ACTIVE : 정상, LOCKED : 잠김, EXPIRED : 만료, WITHDRAWAL : 탈퇴 + + private ZonedDateTime dateWithdrawal; + + private String userEmail; + private Long createdUid; + private Long updatedUid; + +} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/AuditLogRepository.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/AuditLogRepository.java new file mode 100644 index 00000000..1bffa354 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/AuditLogRepository.java @@ -0,0 +1,6 @@ +package com.kamco.cd.kamcoback.postgres.repository.log; + +import com.kamco.cd.kamcoback.postgres.entity.AuditLogEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AuditLogRepository extends JpaRepository, AuditLogRepositoryCustom {} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/AuditLogRepositoryCustom.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/AuditLogRepositoryCustom.java new file mode 100644 index 00000000..b979ff97 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/AuditLogRepositoryCustom.java @@ -0,0 +1,21 @@ +package com.kamco.cd.kamcoback.postgres.repository.log; + +import com.kamco.cd.kamcoback.log.dto.AuditLogDto; +import org.springframework.data.domain.Page; + +import java.time.LocalDate; + +public interface AuditLogRepositoryCustom { + + Page findLogByDaily(AuditLogDto.DailySearchReq searchReq, LocalDate startDate, LocalDate endDate); + + Page findLogByMenu(AuditLogDto.MenuUserSearchReq searchReq, String searchValue); + + Page findLogByAccount(AuditLogDto.MenuUserSearchReq searchReq, String searchValue); + + Page findLogByDailyResult(AuditLogDto.DailySearchReq searchReq, LocalDate logDate); + + Page findLogByMenuResult(AuditLogDto.MenuUserSearchReq searchReq, String menuId); + + Page findLogByAccountResult(AuditLogDto.MenuUserSearchReq searchReq, Long accountId); +} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/AuditLogRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/AuditLogRepositoryImpl.java new file mode 100644 index 00000000..06f4aa43 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/AuditLogRepositoryImpl.java @@ -0,0 +1,427 @@ +package com.kamco.cd.kamcoback.postgres.repository.log; + +import com.kamco.cd.kamcoback.log.dto.AuditLogDto; +import com.kamco.cd.kamcoback.log.dto.ErrorLogDto; +import com.kamco.cd.kamcoback.log.dto.EventStatus; +import com.kamco.cd.kamcoback.log.dto.EventType; +import com.kamco.cd.kamcoback.postgres.entity.AuditLogEntity; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.*; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import io.micrometer.common.util.StringUtils; +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 java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Objects; + +import static com.kamco.cd.kamcoback.postgres.entity.QUserEntity.userEntity; +import static com.kamco.cd.kamcoback.postgres.entity.QAuditLogEntity.auditLogEntity; +import static com.kamco.cd.kamcoback.postgres.entity.QErrorLogEntity.errorLogEntity; +import static com.kamco.cd.kamcoback.postgres.entity.QMenuEntity.menuEntity; +import com.kamco.cd.kamcoback.postgres.entity.QMenuEntity; + +public class AuditLogRepositoryImpl extends QuerydslRepositorySupport implements AuditLogRepositoryCustom { + 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.DailySearchReq searchReq, LocalDate startDate, LocalDate endDate) { + DateTimeExpression groupDateTime = + Expressions.dateTimeTemplate(LocalDateTime.class, "date_trunc('day', {0})", auditLogEntity.createdDate); + + Pageable pageable = searchReq.toPageable(); + List foundContent = queryFactory + .select( + Projections.constructor( + AuditLogDto.AuditList.class, + groupDateTime.as("baseDate"), + readCount().as("readCount"), + cudCount().as("cudCount"), + printCount().as("printCount"), + downloadCount().as("downloadCount"), + auditLogEntity.count().as("totalCount") + ) + ) + .from(auditLogEntity) + .where( + eventEndedAtBetween(startDate, endDate) + ) + .groupBy(groupDateTime) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(groupDateTime.desc()) + .fetch(); + + Long countQuery = queryFactory + .select(groupDateTime.countDistinct()) + .from(auditLogEntity) + .where( + eventEndedAtBetween(startDate, endDate) + ) + .fetchOne(); + + return new PageImpl<>(foundContent, pageable, countQuery); + } + + @Override + public Page findLogByMenu(AuditLogDto.MenuUserSearchReq searchReq, String searchValue) { + Pageable pageable = searchReq.toPageable(); + List foundContent = queryFactory + .select( + Projections.constructor( + AuditLogDto.AuditList.class, + auditLogEntity.menuUid.as("menuId"), + menuEntity.menuNm.max().as("menuName"), + readCount().as("readCount"), + cudCount().as("cudCount"), + printCount().as("printCount"), + downloadCount().as("downloadCount"), + auditLogEntity.count().as("totalCount") + ) + ) + .from(auditLogEntity) + .leftJoin(menuEntity).on(auditLogEntity.menuUid.eq(menuEntity.menuUid)) + .where( + menuNameEquals(searchValue) + ) + .groupBy(auditLogEntity.menuUid) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(auditLogEntity.createdDate.max().desc()) + .fetch(); + + // count query group by 를 지정하면 하나의 row 가 아니라 그룹핑된 여러 row 가 나올 수 있다. + // select query 의 group by 대상의 컬럼을 count query 에선 select distinct 로 처리 한다. + Long countQuery = queryFactory + .select(auditLogEntity.menuUid.countDistinct()) + .from(auditLogEntity) + .leftJoin(menuEntity).on(auditLogEntity.menuUid.eq(menuEntity.menuUid)) + .where(menuNameEquals(searchValue)) + .fetchOne(); + + return new PageImpl<>(foundContent, pageable, countQuery); + } + + @Override + public Page findLogByAccount(AuditLogDto.MenuUserSearchReq searchReq, String searchValue) { + Pageable pageable = searchReq.toPageable(); + List foundContent = queryFactory + .select( + Projections.constructor( + AuditLogDto.AuditList.class, + auditLogEntity.userUid.as("accountId"), + userEntity.userId.as("loginId"), + userEntity.userNm.as("username"), + readCount().as("readCount"), + cudCount().as("cudCount"), + printCount().as("printCount"), + downloadCount().as("downloadCount"), + auditLogEntity.count().as("totalCount") + ) + ) + .from(auditLogEntity) + .leftJoin(userEntity).on(auditLogEntity.userUid.eq(userEntity.id)) + .where( + loginIdOrUsernameContains(searchValue) + ) + .groupBy(auditLogEntity.userUid, userEntity.userId, userEntity.userNm) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) +// .orderBy(auditLogEntity.eventEndedAt.max().desc()) + .fetch(); + + Long countQuery = queryFactory + .select(auditLogEntity.userUid.countDistinct()) + .from(auditLogEntity) + .leftJoin(userEntity).on(auditLogEntity.userUid.eq(userEntity.id)) + .where(loginIdOrUsernameContains(searchValue)) + .fetchOne(); + + return new PageImpl<>(foundContent, pageable, countQuery); + } + + @Override + public Page findLogByDailyResult(AuditLogDto.DailySearchReq searchReq, LocalDate logDate) { + Pageable pageable = searchReq.toPageable(); + QMenuEntity parent = new QMenuEntity("parent"); + // 1depth menu name + StringExpression parentMenuName = + new CaseBuilder() + .when(parent.menuUid.isNull()).then(menuEntity.menuNm) + .otherwise(parent.menuNm); + + // 2depth menu name + StringExpression menuName = + new CaseBuilder() + .when(parent.menuUid.isNull()).then(NULL_STRING) + .otherwise(menuEntity.menuNm); + + List foundContent = queryFactory + .select( + Projections.constructor( + AuditLogDto.AuditDetail.class, + auditLogEntity.id.as("logId"), + userEntity.userNm.as("userName"), + userEntity.userId.as("loginId"), + menuEntity.menuNm.as("menuName"), + auditLogEntity.eventType.as("eventType"), + Projections.constructor( + AuditLogDto.LogDetail.class, + Expressions.constant("한국자산관리공사"), //serviceName + parentMenuName.as("parentMenuName"), + menuName, + menuEntity.menuUrl.as("menuUrl"), + menuEntity.description.as("menuDescription"), + menuEntity.menuOrder.as("sortOrder"), + menuEntity.isUse.as("used") + ) + ) + ) + .from(auditLogEntity) + .leftJoin(menuEntity).on(auditLogEntity.menuUid.eq(menuEntity.menuUid)) + .leftJoin(menuEntity.parent, parent) + .leftJoin(userEntity).on(auditLogEntity.userUid.eq(userEntity.id)) + .where( + eventEndedAtEqDate(logDate) + ) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(auditLogEntity.createdDate.desc()) + .fetch(); + + Long countQuery = queryFactory + .select(auditLogEntity.id.countDistinct()) + .from(auditLogEntity) + .leftJoin(menuEntity).on(auditLogEntity.menuUid.eq(menuEntity.menuUid)) + .leftJoin(menuEntity.parent, parent) + .leftJoin(userEntity).on(auditLogEntity.userUid.eq(userEntity.id)) + .where( + eventEndedAtEqDate(logDate) + ) + .fetchOne(); + + return new PageImpl<>(foundContent, pageable, countQuery); + } + + @Override + public Page findLogByMenuResult(AuditLogDto.MenuUserSearchReq searchReq, String menuUid) { + Pageable pageable = searchReq.toPageable(); + QMenuEntity parent = new QMenuEntity("parent"); + // 1depth menu name + StringExpression parentMenuName = + new CaseBuilder() + .when(parent.menuUid.isNull()).then(menuEntity.menuNm) + .otherwise(parent.menuNm); + + // 2depth menu name + StringExpression menuName = + new CaseBuilder() + .when(parent.menuUid.isNull()).then(NULL_STRING) + .otherwise(menuEntity.menuNm); + + List foundContent = queryFactory + .select( + Projections.constructor( + AuditLogDto.AuditDetail.class, + auditLogEntity.id.as("logId"), + auditLogEntity.createdDate.as("logDateTime"), + userEntity.userNm.as("userName"), + userEntity.userId.as("loginId"), + auditLogEntity.eventType.as("eventType"), + Projections.constructor( + AuditLogDto.LogDetail.class, + Expressions.constant("한국자산관리공사"), //serviceName + parentMenuName.as("parentMenuName"), + menuName, + menuEntity.menuUrl.as("menuUrl"), + menuEntity.description.as("menuDescription"), + menuEntity.menuOrder.as("sortOrder"), + menuEntity.isUse.as("used") + ) + ) + ) + .from(auditLogEntity) + .leftJoin(menuEntity).on(auditLogEntity.menuUid.eq(menuEntity.menuUid)) + .leftJoin(menuEntity.parent, parent) + .leftJoin(userEntity).on(auditLogEntity.userUid.eq(userEntity.id)) + .where( + menuUidEq(menuUid) + ) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(auditLogEntity.createdDate.desc()) + .fetch(); + + Long countQuery = queryFactory + .select(auditLogEntity.id.countDistinct()) + .from(auditLogEntity) + .leftJoin(menuEntity).on(auditLogEntity.menuUid.eq(menuEntity.menuUid)) + .leftJoin(menuEntity.parent, parent) + .leftJoin(userEntity).on(auditLogEntity.userUid.eq(userEntity.id)) + .where( + menuUidEq(menuUid) + ).fetchOne(); + + return new PageImpl<>(foundContent, pageable, countQuery); + } + + @Override + public Page findLogByAccountResult(AuditLogDto.MenuUserSearchReq searchReq, Long userUid) { + Pageable pageable = searchReq.toPageable(); + QMenuEntity parent = new QMenuEntity("parent"); + // 1depth menu name + StringExpression parentMenuName = + new CaseBuilder() + .when(parent.menuUid.isNull()).then(menuEntity.menuNm) + .otherwise(parent.menuNm); + + // 2depth menu name + StringExpression menuName = + new CaseBuilder() + .when(parent.menuUid.isNull()).then(NULL_STRING) + .otherwise(menuEntity.menuNm); + + List foundContent = queryFactory + .select( + Projections.constructor( + AuditLogDto.AuditDetail.class, + auditLogEntity.id.as("logId"), + auditLogEntity.createdDate.as("logDateTime"), + menuEntity.menuNm.as("menuName"), + auditLogEntity.eventType.as("eventType"), + Projections.constructor( + AuditLogDto.LogDetail.class, + Expressions.constant("한국자산관리공사"), //serviceName + parentMenuName.as("parentMenuName"), + menuName, + menuEntity.menuUrl.as("menuUrl"), + menuEntity.description.as("menuDescription"), + menuEntity.menuOrder.as("sortOrder"), + menuEntity.isUse.as("used") + ) + ) + ) + .from(auditLogEntity) + .leftJoin(menuEntity).on(auditLogEntity.menuUid.eq(menuEntity.menuUid)) + .leftJoin(menuEntity.parent, parent) + .leftJoin(userEntity).on(auditLogEntity.userUid.eq(userEntity.id)) + .where( + userUidEq(userUid) + ) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(auditLogEntity.createdDate.desc()) + .fetch(); + + Long countQuery = queryFactory + .select(auditLogEntity.id.countDistinct()) + .from(auditLogEntity) + .leftJoin(menuEntity).on(auditLogEntity.menuUid.eq(menuEntity.menuUid)) + .leftJoin(menuEntity.parent, parent) + .leftJoin(userEntity).on(auditLogEntity.userUid.eq(userEntity.id)) + .where( + userUidEq(userUid) + ) + .fetchOne(); + + return new PageImpl<>(foundContent, pageable, countQuery); + } + + private BooleanExpression eventEndedAtBetween(LocalDate startDate, LocalDate endDate) { + if (Objects.isNull(startDate) || Objects.isNull(endDate)) { + return null; + } + LocalDateTime startDateTime = startDate.atStartOfDay(); + LocalDateTime endDateTime = endDate.plusDays(1).atStartOfDay(); + return auditLogEntity.createdDate.goe(ZonedDateTime.from(startDateTime)) + .and(auditLogEntity.createdDate.lt(ZonedDateTime.from(endDateTime))); + } + + private BooleanExpression menuNameEquals(String searchValue) { + if (StringUtils.isBlank(searchValue)) { + return null; + } + return menuEntity.menuNm.contains(searchValue); + } + + private BooleanExpression loginIdOrUsernameContains(String searchValue) { + if (StringUtils.isBlank(searchValue)) { + return null; + } + return userEntity.userId.contains(searchValue).or(userEntity.userNm.contains(searchValue)); + } + + private BooleanExpression eventStatusEqFailed() { + return auditLogEntity.eventStatus.eq(EventStatus.FAILED); + } + + private BooleanExpression eventTypeEq(EventType eventType) { + if (Objects.isNull(eventType)) { + return null; + } + return auditLogEntity.eventType.eq(eventType); + } + + private BooleanExpression errorLevelEq(ErrorLogDto.LogErrorLevel level) { + if (Objects.isNull(level)) { + return null; + } + return errorLogEntity.errorLevel.eq(ErrorLogDto.LogErrorLevel.valueOf(level.name())); + } + + private BooleanExpression eventEndedAtEqDate(LocalDate logDate) { + DateTimeExpression eventEndedDate = + Expressions.dateTimeTemplate(LocalDateTime.class, "date_trunc('day', {0})", auditLogEntity.createdDate); + LocalDateTime comparisonDate = logDate.atStartOfDay(); + + return eventEndedDate.eq(comparisonDate); + } + + private BooleanExpression menuUidEq(String menuUid) { + return auditLogEntity.menuUid.eq(menuUid); + } + + private BooleanExpression userUidEq(Long userUid) { + return auditLogEntity.userUid.eq(userUid); + } + + private NumberExpression readCount() { + return new CaseBuilder() + .when(auditLogEntity.eventType.eq(EventType.READ)).then(1) + .otherwise(0) + .sum(); + } + + private NumberExpression cudCount() { + return new CaseBuilder() + .when(auditLogEntity.eventType.in(EventType.CREATE, EventType.UPDATE, EventType.DELETE)).then(1) + .otherwise(0) + .sum(); + } + + private NumberExpression printCount() { + return new CaseBuilder() + .when(auditLogEntity.eventType.eq(EventType.PRINT)).then(1) + .otherwise(0) + .sum(); + } + + private NumberExpression downloadCount() { + return new CaseBuilder() + .when(auditLogEntity.eventType.eq(EventType.DOWNLOAD)).then(1) + .otherwise(0) + .sum(); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/ErrorLogRepository.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/ErrorLogRepository.java new file mode 100644 index 00000000..b617f5dd --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/ErrorLogRepository.java @@ -0,0 +1,6 @@ +package com.kamco.cd.kamcoback.postgres.repository.log; + +import com.kamco.cd.kamcoback.postgres.entity.ErrorLogEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ErrorLogRepository extends JpaRepository, ErrorLogRepositoryCustom {} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/ErrorLogRepositoryCustom.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/ErrorLogRepositoryCustom.java new file mode 100644 index 00000000..0aca6186 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/ErrorLogRepositoryCustom.java @@ -0,0 +1,12 @@ +package com.kamco.cd.kamcoback.postgres.repository.log; + +import com.kamco.cd.kamcoback.log.dto.AuditLogDto; +import com.kamco.cd.kamcoback.log.dto.ErrorLogDto; +import com.kamco.cd.kamcoback.postgres.entity.AuditLogEntity; +import org.springframework.data.domain.Page; + +import java.time.LocalDate; + +public interface ErrorLogRepositoryCustom { + public Page findLogByError(ErrorLogDto.ErrorSearchReq searchReq); +} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/ErrorLogRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/ErrorLogRepositoryImpl.java new file mode 100644 index 00000000..35054b63 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/log/ErrorLogRepositoryImpl.java @@ -0,0 +1,117 @@ +package com.kamco.cd.kamcoback.postgres.repository.log; + +import com.kamco.cd.kamcoback.log.dto.ErrorLogDto; +import com.kamco.cd.kamcoback.log.dto.EventStatus; +import com.kamco.cd.kamcoback.log.dto.EventType; +import com.kamco.cd.kamcoback.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 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 java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Objects; + +import static com.kamco.cd.kamcoback.postgres.entity.QAuditLogEntity.auditLogEntity; +import static com.kamco.cd.kamcoback.postgres.entity.QErrorLogEntity.errorLogEntity; +import static com.kamco.cd.kamcoback.postgres.entity.QMenuEntity.menuEntity; +import static com.kamco.cd.kamcoback.postgres.entity.QUserEntity.userEntity; + +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(); + List foundContent = queryFactory + .select( + Projections.constructor( + ErrorLogDto.Basic.class, + errorLogEntity.id.as("logId"), + Expressions.constant("한국자산관리공사"), //serviceName + menuEntity.menuNm.as("menuName"), + userEntity.userId.as("loginId"), + userEntity.userNm.as("userName"), + errorLogEntity.errorType.as("eventType"), + errorLogEntity.errorMessage.as("errorName"), // 기존에는 errorName 값이 있었는데 신규 테이블에는 없음. 에러 메세지와 동일 + errorLogEntity.errorLevel.as("errorLevel"), + errorLogEntity.errorCode.as("errorCode"), + errorLogEntity.errorMessage.as("errorMessage"), + errorLogEntity.stackTrace.as("errorDetail"), + errorLogEntity.createdDate + ) + ) + .from(errorLogEntity) + .leftJoin(auditLogEntity).on(errorLogEntity.id.eq(auditLogEntity.errorLogUid)) + .leftJoin(menuEntity).on(auditLogEntity.menuUid.eq(menuEntity.menuUid)) + .leftJoin(userEntity).on(errorLogEntity.handlerUid.eq(userEntity.id)) + .where( + eventStatusEqFailed(), + eventEndedAtBetween(searchReq.getStartDate(), searchReq.getEndDate()), + eventTypeEq(searchReq.getEventType()), + errorLevelEq(searchReq.getErrorLevel()) + ) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(errorLogEntity.createdDate.desc()) + .fetch(); + + Long countQuery = queryFactory + .select(errorLogEntity.id.countDistinct()) + .from(errorLogEntity) + .leftJoin(auditLogEntity).on(errorLogEntity.id.eq(auditLogEntity.errorLogUid)) + .leftJoin(menuEntity).on(auditLogEntity.menuUid.eq(menuEntity.menuUid)) + .leftJoin(userEntity).on(errorLogEntity.handlerUid.eq(userEntity.id)) + .where( + eventStatusEqFailed(), + eventEndedAtBetween(searchReq.getStartDate(), searchReq.getEndDate()), + eventTypeEq(searchReq.getEventType()), + errorLevelEq(searchReq.getErrorLevel()) + ) + .fetchOne(); + + return new PageImpl<>(foundContent, pageable, countQuery); + } + + private BooleanExpression eventEndedAtBetween(LocalDate startDate, LocalDate endDate) { + if (Objects.isNull(startDate) || Objects.isNull(endDate)) { + return null; + } + LocalDateTime startDateTime = startDate.atStartOfDay(); + LocalDateTime endDateTime = endDate.plusDays(1).atStartOfDay(); + return auditLogEntity.createdDate.goe(ZonedDateTime.from(startDateTime)) + .and(auditLogEntity.createdDate.lt(ZonedDateTime.from(endDateTime))); + } + + private BooleanExpression eventStatusEqFailed() { + return auditLogEntity.eventStatus.eq(EventStatus.FAILED); + } + + private BooleanExpression eventTypeEq(EventType eventType) { + if (Objects.isNull(eventType)) { + return null; + } + return auditLogEntity.eventType.eq(eventType); + } + + private BooleanExpression errorLevelEq(ErrorLogDto.LogErrorLevel level) { + if (Objects.isNull(level)) { + return null; + } + return errorLogEntity.errorLevel.eq(ErrorLogDto.LogErrorLevel.valueOf(level.name())); + } +}