API 로그저장, ExceptionHandler 저장, 감사,에러로그API 작업 진행중

This commit is contained in:
2025-11-20 14:36:59 +09:00
parent aaabd85c9c
commit 107bd6b20f
23 changed files with 1791 additions and 8 deletions

View File

@@ -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<String> handlerEntityNotFoundException(EntityNotFoundException e) {
public ApiResponseDto<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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);
}
}

View File

@@ -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();
}
}

View File

@@ -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<String, String[]> 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;
}
}

View File

@@ -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<Object> {
private final AuditLogRepository auditLogRepository;
public ApiResponseAdvice(AuditLogRepository auditLogRepository) {
this.auditLogRepository = auditLogRepository;
}
@Override
public boolean supports(
MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
@@ -32,9 +45,25 @@ public class ApiResponseAdvice implements ResponseBodyAdvice<Object> {
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;

View File

@@ -20,7 +20,9 @@ public class ApiResponseDto<T> {
@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<T> {
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<T> {
public static ApiResponseDto<String> createException(ApiResponseCode code, String message) {
return new ApiResponseDto<>(code, message);
}
public static ApiResponseDto<String> createException(ApiResponseCode code, String message, HttpStatus httpStatus) {
return new ApiResponseDto<>(code, message, httpStatus);
}
public static ApiResponseDto<String> createException(ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) {
return new ApiResponseDto<>(code, message, httpStatus, errorLogUid);
}
public static <T> ApiResponseDto<T> createException(
ApiResponseCode code, String message, T data) {
@@ -89,6 +106,7 @@ public class ApiResponseDto<T> {
// @formatter:off
OK("요청이 성공하였습니다."),
BAD_REQUEST("요청 파라미터가 잘못되었습니다."),
BAD_GATEWAY("네트워크 상태가 불안정합니다."),
ALREADY_EXIST_MALL("이미 등록된 쇼핑센터입니다."),
NOT_FOUND_MAP("지도를 찾을 수 없습니다."),
UNAUTHORIZED("권한이 없습니다."),
@@ -111,8 +129,8 @@ public class ApiResponseDto<T> {
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<T> {
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();
}
}
}

View File

@@ -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<Page<AuditLogDto.AuditList>> 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<AuditLogDto.AuditList> result = auditLogCoreService.getLogByDaily(
searchReq,
startDate,
endDate
);
return ApiResponseDto.ok(result);
}
@Operation(summary = "일자별 로그 상세")
@GetMapping("/daily/result")
public ApiResponseDto<Page<AuditLogDto.AuditDetail>> 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<AuditLogDto.AuditDetail> result = auditLogCoreService.getLogByDailyResult(
searchReq,
logDate
);
return ApiResponseDto.ok(result);
}
@Operation(summary = "메뉴별 로그 조회")
@GetMapping("/menu")
public ApiResponseDto<Page<AuditLogDto.AuditList>> 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<AuditLogDto.AuditList> result = auditLogCoreService.getLogByMenu(
searchReq, searchValue
);
return ApiResponseDto.ok(result);
}
@Operation(summary = "메뉴별 로그 상세")
@GetMapping("/menu/result")
public ApiResponseDto<Page<AuditLogDto.AuditDetail>> 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<AuditLogDto.AuditDetail> result = auditLogCoreService.getLogByMenuResult(
searchReq,
menuId
);
return ApiResponseDto.ok(result);
}
@Operation(summary = "사용자별 로그 조회")
@GetMapping("/account")
public ApiResponseDto<Page<AuditLogDto.AuditList>> 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<AuditLogDto.AuditList> result = auditLogCoreService.getLogByAccount(
searchReq, searchValue
);
return ApiResponseDto.ok(result);
}
@Operation(summary = "사용자별 로그 상세")
@GetMapping("/account/result")
public ApiResponseDto<Page<AuditLogDto.AuditDetail>> 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<AuditLogDto.AuditDetail> result = auditLogCoreService.getLogByAccountResult(
searchReq,
userUid
);
return ApiResponseDto.ok(result);
}
}

View File

@@ -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<Page<ErrorLogDto.Basic>> 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<ErrorLogDto.Basic> result = errorLogCoreService.findLogByError(searchReq);
return ApiResponseDto.ok(result);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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<AuditLogDto.AuditList, Long, AuditLogDto.DailySearchReq> {
private final AuditLogRepository auditLogRepository;
@Override
public void remove(Long aLong) {
}
@Override
public AuditLogDto.AuditList getOneById(Long aLong) {
return null;
}
@Override
public Page<AuditLogDto.AuditList> search(AuditLogDto.DailySearchReq searchReq) {
return null;
}
public Page<AuditLogDto.AuditList> getLogByDaily(AuditLogDto.DailySearchReq searchRange, LocalDate startDate, LocalDate endDate) {
return auditLogRepository.findLogByDaily(searchRange, startDate, endDate);
}
public Page<AuditLogDto.AuditList> getLogByMenu(AuditLogDto.MenuUserSearchReq searchRange, String searchValue) {
return auditLogRepository.findLogByMenu(searchRange, searchValue);
}
public Page<AuditLogDto.AuditList> getLogByAccount(AuditLogDto.MenuUserSearchReq searchRange, String searchValue) {
return auditLogRepository.findLogByAccount(searchRange, searchValue);
}
public Page<AuditLogDto.AuditDetail> getLogByDailyResult(AuditLogDto.DailySearchReq searchRange, LocalDate logDate) {
return auditLogRepository.findLogByDailyResult(searchRange, logDate);
}
public Page<AuditLogDto.AuditDetail> getLogByMenuResult(AuditLogDto.MenuUserSearchReq searchRange, String menuId) {
return auditLogRepository.findLogByMenuResult(searchRange, menuId);
}
public Page<AuditLogDto.AuditDetail> getLogByAccountResult(AuditLogDto.MenuUserSearchReq searchRange, Long accountId) {
return auditLogRepository.findLogByAccountResult(searchRange, accountId);
}
}

View File

@@ -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<ErrorLogDto.Basic, Long, ErrorLogDto.ErrorSearchReq> {
private final ErrorLogRepository errorLogRepository;
public Page<ErrorLogDto.Basic> 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<ErrorLogDto.Basic> search(ErrorLogDto.ErrorSearchReq searchReq) {
return null;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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<MenuEntity> children = new ArrayList<>();
}

View File

@@ -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;
}

View File

@@ -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<AuditLogEntity, Long>, AuditLogRepositoryCustom {}

View File

@@ -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<AuditLogDto.AuditList> findLogByDaily(AuditLogDto.DailySearchReq searchReq, LocalDate startDate, LocalDate endDate);
Page<AuditLogDto.AuditList> findLogByMenu(AuditLogDto.MenuUserSearchReq searchReq, String searchValue);
Page<AuditLogDto.AuditList> findLogByAccount(AuditLogDto.MenuUserSearchReq searchReq, String searchValue);
Page<AuditLogDto.AuditDetail> findLogByDailyResult(AuditLogDto.DailySearchReq searchReq, LocalDate logDate);
Page<AuditLogDto.AuditDetail> findLogByMenuResult(AuditLogDto.MenuUserSearchReq searchReq, String menuId);
Page<AuditLogDto.AuditDetail> findLogByAccountResult(AuditLogDto.MenuUserSearchReq searchReq, Long accountId);
}

View File

@@ -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<AuditLogDto.AuditList> findLogByDaily(AuditLogDto.DailySearchReq searchReq, LocalDate startDate, LocalDate endDate) {
DateTimeExpression<LocalDateTime> groupDateTime =
Expressions.dateTimeTemplate(LocalDateTime.class, "date_trunc('day', {0})", auditLogEntity.createdDate);
Pageable pageable = searchReq.toPageable();
List<AuditLogDto.AuditList> 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<AuditLogDto.AuditList> findLogByMenu(AuditLogDto.MenuUserSearchReq searchReq, String searchValue) {
Pageable pageable = searchReq.toPageable();
List<AuditLogDto.AuditList> 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<AuditLogDto.AuditList> findLogByAccount(AuditLogDto.MenuUserSearchReq searchReq, String searchValue) {
Pageable pageable = searchReq.toPageable();
List<AuditLogDto.AuditList> 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<AuditLogDto.AuditDetail> 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<AuditLogDto.AuditDetail> 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<AuditLogDto.AuditDetail> 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<AuditLogDto.AuditDetail> 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<AuditLogDto.AuditDetail> 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<AuditLogDto.AuditDetail> 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<LocalDateTime> 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<Integer> readCount() {
return new CaseBuilder()
.when(auditLogEntity.eventType.eq(EventType.READ)).then(1)
.otherwise(0)
.sum();
}
private NumberExpression<Integer> cudCount() {
return new CaseBuilder()
.when(auditLogEntity.eventType.in(EventType.CREATE, EventType.UPDATE, EventType.DELETE)).then(1)
.otherwise(0)
.sum();
}
private NumberExpression<Integer> printCount() {
return new CaseBuilder()
.when(auditLogEntity.eventType.eq(EventType.PRINT)).then(1)
.otherwise(0)
.sum();
}
private NumberExpression<Integer> downloadCount() {
return new CaseBuilder()
.when(auditLogEntity.eventType.eq(EventType.DOWNLOAD)).then(1)
.otherwise(0)
.sum();
}
}

View File

@@ -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<ErrorLogEntity, Long>, ErrorLogRepositoryCustom {}

View File

@@ -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<ErrorLogDto.Basic> findLogByError(ErrorLogDto.ErrorSearchReq searchReq);
}

View File

@@ -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<ErrorLogDto.Basic> findLogByError(ErrorLogDto.ErrorSearchReq searchReq) {
Pageable pageable = searchReq.toPageable();
List<ErrorLogDto.Basic> 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()));
}
}