init
This commit is contained in:
11
src/main/java/com/kamco/cd/training/config/CacheConfig.java
Normal file
11
src/main/java/com/kamco/cd/training/config/CacheConfig.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package com.kamco.cd.training.config;
|
||||
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@EnableCaching
|
||||
@Configuration
|
||||
public class CacheConfig {
|
||||
// training 서버는 Redis 사용하지 않고 Spring Boot 메모리 캐시를 사용함
|
||||
// => org.springframework.cache.annotation.Cacheable
|
||||
}
|
||||
27
src/main/java/com/kamco/cd/training/config/FileConfig.java
Normal file
27
src/main/java/com/kamco/cd/training/config/FileConfig.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.kamco.cd.training.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** GeoJSON 파일 모니터링 설정 */
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "file.config")
|
||||
@Getter
|
||||
@Setter
|
||||
public class FileConfig {
|
||||
|
||||
// private String rootDir = "D:\\app/";
|
||||
private String rootDir = "/app/";
|
||||
private String rootSyncDir = rootDir + "original-images/";
|
||||
private String tmpSyncDir = rootSyncDir + "tmp/";
|
||||
|
||||
private String dataSetDir = rootDir + "dataset/";
|
||||
private String tmpDataSetDir = dataSetDir + "tmp/";
|
||||
|
||||
// private String rootSyncDir = "/app/original-images/";
|
||||
// private String tmpSyncDir = rootSyncDir + "tmp/";
|
||||
|
||||
private String syncFileExt = "tfw,tif";
|
||||
}
|
||||
@@ -0,0 +1,528 @@
|
||||
package com.kamco.cd.training.config;
|
||||
|
||||
import com.kamco.cd.training.auth.CustomUserDetails;
|
||||
import com.kamco.cd.training.common.exception.CustomApiException;
|
||||
import com.kamco.cd.training.common.exception.DuplicateFileException;
|
||||
import com.kamco.cd.training.config.api.ApiLogFunction;
|
||||
import com.kamco.cd.training.config.api.ApiResponseDto;
|
||||
import com.kamco.cd.training.config.api.ApiResponseDto.ApiResponseCode;
|
||||
import com.kamco.cd.training.log.dto.ErrorLogDto;
|
||||
import com.kamco.cd.training.members.exception.MemberException;
|
||||
import com.kamco.cd.training.postgres.entity.ErrorLogEntity;
|
||||
import com.kamco.cd.training.postgres.repository.log.ErrorLogRepository;
|
||||
import io.micrometer.core.instrument.config.validate.ValidationException;
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
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;
|
||||
|
||||
@Slf4j
|
||||
@Order(value = 1)
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private final ErrorLogRepository errorLogRepository;
|
||||
|
||||
public GlobalExceptionHandler(ErrorLogRepository errorLogRepository) {
|
||||
this.errorLogRepository = errorLogRepository;
|
||||
}
|
||||
|
||||
@ResponseStatus(HttpStatus.CONFLICT)
|
||||
@ExceptionHandler(DuplicateFileException.class)
|
||||
public ApiResponseDto<String> handleDuplicateFileException(
|
||||
DuplicateFileException e, HttpServletRequest request) {
|
||||
log.warn("[DuplicateFileException] resource :{} ", e.getMessage());
|
||||
this.errorLog(request, e);
|
||||
ApiResponseCode code = ApiResponseCode.CONFLICT;
|
||||
ErrorLogEntity errorLog =
|
||||
saveErrorLogData(
|
||||
request,
|
||||
code,
|
||||
HttpStatus.CONFLICT,
|
||||
ErrorLogDto.LogErrorLevel.WARNING,
|
||||
e.getStackTrace());
|
||||
|
||||
return ApiResponseDto.createException(
|
||||
code, e.getMessage(), HttpStatus.CONFLICT, errorLog.getId());
|
||||
}
|
||||
|
||||
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
|
||||
@ExceptionHandler(ValidationException.class)
|
||||
public ApiResponseDto<String> handleValidationException(
|
||||
ValidationException e, HttpServletRequest request) {
|
||||
log.warn("[ValidationException] resource :{} ", e.getMessage());
|
||||
this.errorLog(request, e);
|
||||
ApiResponseCode code = ApiResponseCode.UNPROCESSABLE_ENTITY;
|
||||
ErrorLogEntity errorLog =
|
||||
saveErrorLogData(
|
||||
request,
|
||||
code,
|
||||
HttpStatus.UNPROCESSABLE_ENTITY,
|
||||
ErrorLogDto.LogErrorLevel.WARNING,
|
||||
e.getStackTrace());
|
||||
|
||||
return ApiResponseDto.createException(
|
||||
code, e.getMessage(), HttpStatus.UNPROCESSABLE_ENTITY, errorLog.getId());
|
||||
}
|
||||
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public ApiResponseDto<String> handleIllegalArgumentException(
|
||||
IllegalArgumentException e, HttpServletRequest request) {
|
||||
log.warn("[IllegalArgumentException] resource :{} ", e.getMessage());
|
||||
this.errorLog(request, e);
|
||||
ApiResponseCode code = ApiResponseCode.BAD_REQUEST;
|
||||
ErrorLogEntity errorLog =
|
||||
saveErrorLogData(
|
||||
request,
|
||||
code,
|
||||
HttpStatus.BAD_REQUEST,
|
||||
ErrorLogDto.LogErrorLevel.WARNING,
|
||||
e.getStackTrace());
|
||||
|
||||
return ApiResponseDto.createException(
|
||||
code, e.getMessage(), HttpStatus.BAD_REQUEST, errorLog.getId());
|
||||
}
|
||||
|
||||
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
|
||||
@ExceptionHandler(EntityNotFoundException.class)
|
||||
public ApiResponseDto<String> handlerEntityNotFoundException(
|
||||
EntityNotFoundException e, HttpServletRequest request) {
|
||||
log.warn("[EntityNotFoundException] resource :{} ", e.getMessage());
|
||||
this.errorLog(request, e);
|
||||
String codeName = "NOT_FOUND_DATA";
|
||||
ErrorLogEntity errorLog =
|
||||
saveErrorLogData(
|
||||
request,
|
||||
ApiResponseCode.getCode(codeName),
|
||||
HttpStatus.valueOf("UNPROCESSABLE_ENTITY"),
|
||||
ErrorLogDto.LogErrorLevel.WARNING,
|
||||
e.getStackTrace());
|
||||
|
||||
return ApiResponseDto.createException(
|
||||
ApiResponseCode.getCode(codeName),
|
||||
ApiResponseCode.getMessage(codeName),
|
||||
HttpStatus.valueOf("UNPROCESSABLE_ENTITY"),
|
||||
errorLog.getId());
|
||||
}
|
||||
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
public ApiResponseDto<String> handleUnreadable(
|
||||
HttpMessageNotReadableException e, HttpServletRequest request) {
|
||||
log.warn("[HttpMessageNotReadableException] resource :{} ", e.getMessage());
|
||||
this.errorLog(request, e);
|
||||
String codeName = "BAD_REQUEST";
|
||||
ErrorLogEntity errorLog =
|
||||
saveErrorLogData(
|
||||
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());
|
||||
this.errorLog(request, e);
|
||||
String codeName = "NOT_FOUND_DATA";
|
||||
ErrorLogEntity errorLog =
|
||||
saveErrorLogData(
|
||||
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.UNPROCESSABLE_ENTITY)
|
||||
@ExceptionHandler(DataIntegrityViolationException.class)
|
||||
public ApiResponseDto<String> handlerDataIntegrityViolationException(
|
||||
DataIntegrityViolationException e, HttpServletRequest request) {
|
||||
log.warn("[DataIntegrityViolationException] resource :{} ", e.getMessage());
|
||||
this.errorLog(request, e);
|
||||
String codeName = "DATA_INTEGRITY_ERROR";
|
||||
ErrorLogEntity errorLog =
|
||||
saveErrorLogData(
|
||||
request,
|
||||
ApiResponseCode.getCode(codeName),
|
||||
HttpStatus.valueOf("UNPROCESSABLE_ENTITY"),
|
||||
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());
|
||||
this.errorLog(request, e);
|
||||
String codeName = "BAD_REQUEST";
|
||||
ErrorLogEntity errorLog =
|
||||
saveErrorLogData(
|
||||
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.FORBIDDEN)
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
public ApiResponseDto<String> handlerAccessDeniedException(
|
||||
AccessDeniedException e, HttpServletRequest request) {
|
||||
log.warn("[AccessDeniedException] resource :{} ", e.getMessage());
|
||||
this.errorLog(request, e);
|
||||
String codeName = "FORBIDDEN";
|
||||
ErrorLogEntity errorLog =
|
||||
saveErrorLogData(
|
||||
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());
|
||||
this.errorLog(request, e);
|
||||
String codeName = "BAD_GATEWAY";
|
||||
ErrorLogEntity errorLog =
|
||||
saveErrorLogData(
|
||||
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.UNPROCESSABLE_ENTITY)
|
||||
@ExceptionHandler(IllegalStateException.class)
|
||||
public ApiResponseDto<String> handlerIllegalStateException(
|
||||
IllegalStateException e, HttpServletRequest request) {
|
||||
log.warn("[IllegalStateException] resource :{} ", e.getMessage());
|
||||
this.errorLog(request, e);
|
||||
String codeName = "UNPROCESSABLE_ENTITY";
|
||||
ErrorLogEntity errorLog =
|
||||
saveErrorLogData(
|
||||
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.BAD_REQUEST)
|
||||
@ExceptionHandler(MemberException.DuplicateMemberException.class)
|
||||
public ApiResponseDto<String> handlerDuplicateMemberException(
|
||||
MemberException.DuplicateMemberException e, HttpServletRequest request) {
|
||||
log.warn("[DuplicateMemberException] resource :{} ", e.getMessage());
|
||||
this.errorLog(request, e);
|
||||
String codeName = "";
|
||||
|
||||
switch (e.getField()) {
|
||||
case EMPLOYEE_NO -> {
|
||||
codeName = "DUPLICATE_EMPLOYEEID";
|
||||
}
|
||||
default -> {
|
||||
codeName = "DUPLICATE_DATA";
|
||||
}
|
||||
}
|
||||
|
||||
ErrorLogEntity errorLog =
|
||||
saveErrorLogData(
|
||||
request,
|
||||
ApiResponseCode.getCode(codeName),
|
||||
HttpStatus.valueOf("BAD_REQUEST"),
|
||||
ErrorLogDto.LogErrorLevel.WARNING,
|
||||
e.getStackTrace());
|
||||
|
||||
return ApiResponseDto.createException(
|
||||
ApiResponseCode.getCode(codeName),
|
||||
ApiResponseCode.getMessage(codeName),
|
||||
HttpStatus.valueOf("BAD_REQUEST"),
|
||||
errorLog.getId());
|
||||
}
|
||||
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
@ExceptionHandler(MemberException.MemberNotFoundException.class)
|
||||
public ApiResponseDto<String> handlerMemberNotFoundException(
|
||||
MemberException.MemberNotFoundException e, HttpServletRequest request) {
|
||||
log.warn("[MemberNotFoundException] resource :{} ", e.getMessage());
|
||||
this.errorLog(request, e);
|
||||
String codeName = "NOT_FOUND_USER";
|
||||
|
||||
ErrorLogEntity errorLog =
|
||||
saveErrorLogData(
|
||||
request,
|
||||
ApiResponseCode.getCode(codeName),
|
||||
HttpStatus.valueOf("BAD_REQUEST"),
|
||||
ErrorLogDto.LogErrorLevel.WARNING,
|
||||
e.getStackTrace());
|
||||
|
||||
return ApiResponseDto.createException(
|
||||
ApiResponseCode.getCode(codeName),
|
||||
ApiResponseCode.getMessage(codeName),
|
||||
HttpStatus.valueOf("BAD_REQUEST"),
|
||||
errorLog.getId());
|
||||
}
|
||||
|
||||
@ResponseStatus(HttpStatus.CONFLICT)
|
||||
@ExceptionHandler(DuplicateKeyException.class)
|
||||
public ApiResponseDto<String> handlerDuplicateKeyException(
|
||||
DuplicateKeyException e, HttpServletRequest request) {
|
||||
log.warn("[DuplicateKeyException] resource :{} ", e.getMessage());
|
||||
this.errorLog(request, e);
|
||||
String codeName = "DUPLICATE_DATA";
|
||||
|
||||
ErrorLogEntity errorLog =
|
||||
saveErrorLogData(
|
||||
request,
|
||||
ApiResponseCode.getCode(codeName),
|
||||
HttpStatus.valueOf("CONFLICT"),
|
||||
ErrorLogDto.LogErrorLevel.WARNING,
|
||||
e.getStackTrace());
|
||||
|
||||
return ApiResponseDto.createException(
|
||||
ApiResponseCode.getCode(codeName),
|
||||
ApiResponseCode.getMessage(codeName),
|
||||
HttpStatus.valueOf("CONFLICT"),
|
||||
errorLog.getId());
|
||||
}
|
||||
|
||||
@ExceptionHandler(BadCredentialsException.class)
|
||||
public ResponseEntity<ApiResponseDto<String>> handleBadCredentials(
|
||||
BadCredentialsException e, HttpServletRequest request) {
|
||||
log.warn("[BadCredentialsException] resource : {} ", e.getMessage());
|
||||
this.errorLog(request, e);
|
||||
String codeName = "UNAUTHORIZED";
|
||||
|
||||
ErrorLogEntity errorLog =
|
||||
saveErrorLogData(
|
||||
request,
|
||||
ApiResponseCode.getCode(codeName),
|
||||
HttpStatus.valueOf(codeName),
|
||||
ErrorLogDto.LogErrorLevel.WARNING,
|
||||
e.getStackTrace());
|
||||
|
||||
ApiResponseDto<String> body =
|
||||
ApiResponseDto.createException(
|
||||
ApiResponseCode.getCode(codeName),
|
||||
ApiResponseCode.getMessage(codeName),
|
||||
HttpStatus.valueOf(codeName),
|
||||
errorLog.getId());
|
||||
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED) // 🔥 여기서 401 지정
|
||||
.body(body);
|
||||
}
|
||||
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public ApiResponseDto<String> handlerRuntimeException(
|
||||
RuntimeException e, HttpServletRequest request) {
|
||||
log.warn("[RuntimeException] resource :{} ", e.getMessage());
|
||||
this.errorLog(request, e);
|
||||
String codeName = "INTERNAL_SERVER_ERROR";
|
||||
ErrorLogEntity errorLog =
|
||||
saveErrorLogData(
|
||||
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());
|
||||
this.errorLog(request, e);
|
||||
String codeName = "INTERNAL_SERVER_ERROR";
|
||||
ErrorLogEntity errorLog =
|
||||
saveErrorLogData(
|
||||
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 saveErrorLogData(
|
||||
HttpServletRequest request,
|
||||
ApiResponseCode errorCode,
|
||||
HttpStatus httpStatus,
|
||||
ErrorLogDto.LogErrorLevel logErrorLevel,
|
||||
StackTraceElement[] stackTrace) {
|
||||
|
||||
Long userid = null;
|
||||
|
||||
/**
|
||||
* servletRequest.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth 이 요청이
|
||||
* JWT 인증을 통과한 요청인가? 그리고 Spring Security Authentication 객체가 UsernamePasswordAuthenticationToken
|
||||
* 타입인가? 체크
|
||||
*/
|
||||
/**
|
||||
* auth.getPrincipal() instanceof CustomUserDetails customUserDetails principal 안에 들어있는 객체가 내가
|
||||
* 만든 CustomUserDetails 타입인가? 체크
|
||||
*/
|
||||
if (request.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth
|
||||
&& auth.getPrincipal() instanceof CustomUserDetails customUserDetails) {
|
||||
|
||||
// audit 에는 long 타입 user_id가 들어가지만 토큰 sub은 uuid여서 user_id 가져오기
|
||||
userid = customUserDetails.getMember().getId();
|
||||
}
|
||||
|
||||
String stackTraceStr =
|
||||
Arrays.stream(stackTrace)
|
||||
.map(StackTraceElement::toString)
|
||||
.collect(Collectors.joining("\n"))
|
||||
.substring(0, Math.min(stackTrace.length, 255));
|
||||
|
||||
ErrorLogEntity errorLogEntity =
|
||||
new ErrorLogEntity(
|
||||
request.getRequestURI(),
|
||||
ApiLogFunction.getEventType(request),
|
||||
logErrorLevel,
|
||||
String.valueOf(httpStatus.value()),
|
||||
errorCode.getText(),
|
||||
stackTraceStr,
|
||||
userid,
|
||||
ZonedDateTime.now());
|
||||
|
||||
return errorLogRepository.save(errorLogEntity);
|
||||
}
|
||||
|
||||
@ExceptionHandler(CustomApiException.class)
|
||||
public ApiResponseDto<String> handleCustomApiException(
|
||||
CustomApiException e, HttpServletRequest request) {
|
||||
log.warn("[CustomApiException] resource : {}", e.getMessage());
|
||||
this.errorLog(request, e);
|
||||
String codeName = e.getCodeName();
|
||||
HttpStatus status = e.getStatus();
|
||||
// String message = e.getMessage() == null ? ApiResponseCode.getMessage(codeName) :
|
||||
// e.getMessage();
|
||||
//
|
||||
// ApiResponseCode apiCode = ApiResponseCode.getCode(codeName);
|
||||
//
|
||||
// ErrorLogEntity errorLog =
|
||||
// saveErrorLogData(
|
||||
// request, apiCode, status, ErrorLogDto.LogErrorLevel.WARNING, e.getStackTrace());
|
||||
//
|
||||
// ApiResponseDto<String> body =
|
||||
// ApiResponseDto.createException(apiCode, message, status, errorLog.getId());
|
||||
|
||||
ErrorLogEntity errorLog =
|
||||
saveErrorLogData(
|
||||
request,
|
||||
ApiResponseCode.getCode(codeName),
|
||||
HttpStatus.valueOf(status.value()),
|
||||
ErrorLogDto.LogErrorLevel.WARNING,
|
||||
e.getStackTrace());
|
||||
|
||||
return ApiResponseDto.createException(
|
||||
ApiResponseCode.getCode(codeName),
|
||||
ApiResponseCode.getMessage(codeName),
|
||||
HttpStatus.valueOf(status.value()),
|
||||
errorLog.getId());
|
||||
|
||||
// return new ResponseEntity<>(body, status);
|
||||
}
|
||||
|
||||
private void errorLog(HttpServletRequest request, Throwable e) {
|
||||
String uri = request != null ? request.getRequestURI() : "N/A";
|
||||
|
||||
// 예외 타입명 (IllegalArgumentException, BadCredentialsException 등)
|
||||
String exceptionName = e.getClass().getSimpleName();
|
||||
|
||||
log.error("[{}] uri={}, message={}", exceptionName, uri, e.getMessage());
|
||||
|
||||
log.error("[{}] stacktrace", exceptionName, e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.kamco.cd.training.config;
|
||||
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.servers.Server;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class OpenApiConfig {
|
||||
|
||||
@Value("${swagger.local-port}")
|
||||
private String localPort;
|
||||
|
||||
@Value("${spring.profiles.active:local}")
|
||||
private String profile;
|
||||
|
||||
@Value("${swagger.dev-url:https://kamco.training-dev-api.gs.dabeeo.com}")
|
||||
private String devUrl;
|
||||
|
||||
@Value("${swagger.prod-url:https://api.training-kamco.com}")
|
||||
private String prodUrl;
|
||||
|
||||
@Bean
|
||||
public OpenAPI kamcoOpenAPI() {
|
||||
// 1) SecurityScheme 정의 (Bearer JWT)
|
||||
SecurityScheme bearerAuth =
|
||||
new SecurityScheme()
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.scheme("bearer")
|
||||
.bearerFormat("JWT")
|
||||
.in(SecurityScheme.In.HEADER)
|
||||
.name("Authorization");
|
||||
|
||||
// 2) SecurityRequirement (기본으로 BearerAuth 사용)
|
||||
SecurityRequirement securityRequirement = new SecurityRequirement().addList("BearerAuth");
|
||||
|
||||
// 3) Components 에 SecurityScheme 등록
|
||||
Components components = new Components().addSecuritySchemes("BearerAuth", bearerAuth);
|
||||
|
||||
// profile 별 server url 분기
|
||||
List<Server> servers = new ArrayList<>();
|
||||
if ("dev".equals(profile)) {
|
||||
servers.add(new Server().url(devUrl).description("개발 서버"));
|
||||
servers.add(new Server().url("http://localhost:" + localPort).description("로컬 서버"));
|
||||
// servers.add(new Server().url(prodUrl).description("운영 서버"));
|
||||
} else if ("prod".equals(profile)) {
|
||||
// servers.add(new Server().url(prodUrl).description("운영 서버"));
|
||||
servers.add(new Server().url("http://localhost:" + localPort).description("로컬 서버"));
|
||||
servers.add(new Server().url(devUrl).description("개발 서버"));
|
||||
} else {
|
||||
servers.add(new Server().url("http://localhost:" + localPort).description("로컬 서버"));
|
||||
servers.add(new Server().url(devUrl).description("개발 서버"));
|
||||
// servers.add(new Server().url(prodUrl).description("운영 서버"));
|
||||
}
|
||||
|
||||
return new OpenAPI()
|
||||
.info(
|
||||
new Info()
|
||||
.title("KAMCO Change Detection API")
|
||||
.description(
|
||||
"KAMCO 변화 탐지 시스템 API 문서\n\n"
|
||||
+ "이 API는 지리공간 데이터를 활용한 변화 탐지 시스템을 제공합니다.\n"
|
||||
+ "GeoJSON 형식의 공간 데이터를 처리하며, PostgreSQL/PostGIS 기반으로 동작합니다.")
|
||||
.version("v1.0.0"))
|
||||
.servers(servers)
|
||||
// 만들어둔 components를 넣어야 함
|
||||
.components(components)
|
||||
.addSecurityItem(securityRequirement);
|
||||
}
|
||||
}
|
||||
136
src/main/java/com/kamco/cd/training/config/SecurityConfig.java
Normal file
136
src/main/java/com/kamco/cd/training/config/SecurityConfig.java
Normal file
@@ -0,0 +1,136 @@
|
||||
package com.kamco.cd.training.config;
|
||||
|
||||
import com.kamco.cd.training.auth.CustomAuthenticationProvider;
|
||||
import com.kamco.cd.training.auth.JwtAuthenticationFilter;
|
||||
import java.util.List;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.firewall.HttpFirewall;
|
||||
import org.springframework.security.web.firewall.StrictHttpFirewall;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(
|
||||
org.springframework.security.config.annotation.web.builders.HttpSecurity http,
|
||||
JwtAuthenticationFilter jwtAuthenticationFilter,
|
||||
CustomAuthenticationProvider customAuthenticationProvider)
|
||||
throws Exception {
|
||||
|
||||
http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.formLogin(form -> form.disable())
|
||||
|
||||
// /monitor 에서 Basic 인증을 쓰려면 disable 하면 안됨
|
||||
.httpBasic(basic -> {})
|
||||
.logout(logout -> logout.disable())
|
||||
.authenticationProvider(customAuthenticationProvider)
|
||||
.authorizeHttpRequests(
|
||||
auth ->
|
||||
auth
|
||||
|
||||
// monitor
|
||||
.requestMatchers("/monitor/health", "/monitor/health/**")
|
||||
.permitAll()
|
||||
.requestMatchers("/monitor/**")
|
||||
.authenticated() // Basic으로 인증되게끔
|
||||
|
||||
// mapsheet
|
||||
.requestMatchers("/api/mapsheet/**")
|
||||
.permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/api/mapsheet/upload")
|
||||
.permitAll()
|
||||
|
||||
// test role
|
||||
.requestMatchers("/api/test/admin")
|
||||
.hasRole("ADMIN")
|
||||
.requestMatchers("/api/test/label")
|
||||
.hasAnyRole("ADMIN", "LABELER")
|
||||
.requestMatchers("/api/test/review")
|
||||
.hasAnyRole("ADMIN", "REVIEWER")
|
||||
|
||||
// common permit
|
||||
.requestMatchers("/error")
|
||||
.permitAll()
|
||||
.requestMatchers(HttpMethod.OPTIONS, "/**")
|
||||
.permitAll()
|
||||
.requestMatchers(
|
||||
"/api/auth/signin",
|
||||
"/api/auth/refresh",
|
||||
"/api/auth/logout",
|
||||
"/swagger-ui/**",
|
||||
"/v3/api-docs/**",
|
||||
"/api/members/*/password",
|
||||
"/api/upload/chunk-upload-dataset",
|
||||
"/api/upload/chunk-upload-complete")
|
||||
.permitAll()
|
||||
|
||||
// default
|
||||
.anyRequest()
|
||||
.authenticated())
|
||||
|
||||
// JWT 필터는 앞단에
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
|
||||
throws Exception {
|
||||
return configuration.getAuthenticationManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
/** CORS 설정 */
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration config = new CorsConfiguration(); // CORS 객체 생성
|
||||
config.setAllowedOriginPatterns(List.of("*")); // 도메인 허용
|
||||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
||||
config.setAllowedHeaders(List.of("*")); // 헤더요청 Authorization, Content-Type, X-Custom-Header
|
||||
config.setAllowCredentials(true); // 쿠키, Authorization 헤더, Bearer Token 등 자격증명 포함 요청을 허용할지 설정
|
||||
config.setExposedHeaders(List.of("Content-Disposition"));
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
/** "/**" → 모든 API 경로에 대해 이 CORS 규칙을 적용 /api/** 같이 특정 경로만 지정 가능. */
|
||||
source.registerCorsConfiguration("/**", config); // CORS 정책을 등록
|
||||
return source;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HttpFirewall httpFirewall() {
|
||||
StrictHttpFirewall firewall = new StrictHttpFirewall();
|
||||
firewall.setAllowUrlEncodedSlash(true);
|
||||
firewall.setAllowUrlEncodedDoubleSlash(true);
|
||||
firewall.setAllowUrlEncodedPercent(true);
|
||||
firewall.setAllowSemicolon(true);
|
||||
return firewall;
|
||||
}
|
||||
|
||||
/** 완전 제외(필터 자체를 안 탐) */
|
||||
@Bean
|
||||
public WebSecurityCustomizer webSecurityCustomizer() {
|
||||
return (web) -> web.ignoring().requestMatchers("/api/mapsheet/**");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.kamco.cd.training.config;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import javax.sql.DataSource;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class StartupLogger {
|
||||
|
||||
private final Environment environment;
|
||||
private final DataSource dataSource;
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void logStartupInfo() {
|
||||
String[] activeProfiles = environment.getActiveProfiles();
|
||||
String profileInfo = activeProfiles.length > 0 ? String.join(", ", activeProfiles) : "default";
|
||||
|
||||
// Database connection information
|
||||
String dbUrl = environment.getProperty("spring.datasource.url");
|
||||
String dbUsername = environment.getProperty("spring.datasource.username");
|
||||
String dbDriver = environment.getProperty("spring.datasource.driver-class-name");
|
||||
|
||||
// HikariCP pool settings
|
||||
String poolInfo = "";
|
||||
if (dataSource instanceof HikariDataSource hikariDs) {
|
||||
poolInfo =
|
||||
String.format(
|
||||
"""
|
||||
│ Pool Size : min=%d, max=%d
|
||||
│ Connection Timeout: %dms
|
||||
│ Idle Timeout : %dms
|
||||
│ Max Lifetime : %dms""",
|
||||
hikariDs.getMinimumIdle(),
|
||||
hikariDs.getMaximumPoolSize(),
|
||||
hikariDs.getConnectionTimeout(),
|
||||
hikariDs.getIdleTimeout(),
|
||||
hikariDs.getMaxLifetime());
|
||||
}
|
||||
|
||||
// JPA/Hibernate settings
|
||||
String showSql = environment.getProperty("spring.jpa.show-sql", "false");
|
||||
String ddlAuto = environment.getProperty("spring.jpa.hibernate.ddl-auto", "none");
|
||||
String batchSize =
|
||||
environment.getProperty("spring.jpa.properties.hibernate.jdbc.batch_size", "N/A");
|
||||
String batchFetchSize =
|
||||
environment.getProperty("spring.jpa.properties.hibernate.default_batch_fetch_size", "N/A");
|
||||
|
||||
String startupMessage =
|
||||
String.format(
|
||||
"""
|
||||
|
||||
╔════════════════════════════════════════════════════════════════════════════════╗
|
||||
║ 🚀 APPLICATION STARTUP INFORMATION ║
|
||||
╠════════════════════════════════════════════════════════════════════════════════╣
|
||||
║ PROFILE CONFIGURATION ║
|
||||
╠────────────────────────────────────────────────────────────────────────────────╣
|
||||
│ Active Profile(s): %s
|
||||
╠════════════════════════════════════════════════════════════════════════════════╣
|
||||
║ DATABASE CONFIGURATION ║
|
||||
╠────────────────────────────────────────────────────────────────────────────────╣
|
||||
│ Database URL : %s
|
||||
│ Username : %s
|
||||
│ Driver : %s
|
||||
╠════════════════════════════════════════════════════════════════════════════════╣
|
||||
║ HIKARICP CONNECTION POOL ║
|
||||
╠────────────────────────────────────────────────────────────────────────────────╣
|
||||
%s
|
||||
╠════════════════════════════════════════════════════════════════════════════════╣
|
||||
║ JPA/HIBERNATE CONFIGURATION ║
|
||||
╠────────────────────────────────────────────────────────────────────────────────╣
|
||||
│ Show SQL : %s
|
||||
│ DDL Auto : %s
|
||||
│ JDBC Batch Size : %s
|
||||
│ Fetch Batch Size : %s
|
||||
╚════════════════════════════════════════════════════════════════════════════════╝
|
||||
""",
|
||||
profileInfo,
|
||||
dbUrl != null ? dbUrl : "N/A",
|
||||
dbUsername != null ? dbUsername : "N/A",
|
||||
dbDriver != null ? dbDriver : "PostgreSQL JDBC Driver (auto-detected)",
|
||||
poolInfo,
|
||||
showSql,
|
||||
ddlAuto,
|
||||
batchSize,
|
||||
batchFetchSize);
|
||||
|
||||
log.info(startupMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.kamco.cd.training.config;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityScheme;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@SecurityScheme(
|
||||
name = "BearerAuth",
|
||||
type = SecuritySchemeType.HTTP,
|
||||
scheme = "bearer",
|
||||
bearerFormat = "JWT")
|
||||
public class SwaggerConfig {}
|
||||
32
src/main/java/com/kamco/cd/training/config/WebConfig.java
Normal file
32
src/main/java/com/kamco/cd/training/config/WebConfig.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package com.kamco.cd.training.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.kamco.cd.training.common.utils.geometry.GeometryDeserializer;
|
||||
import com.kamco.cd.training.common.utils.geometry.GeometrySerializer;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.Polygon;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Bean
|
||||
public ObjectMapper objectMapper() {
|
||||
SimpleModule module = new SimpleModule();
|
||||
module.addSerializer(Geometry.class, new GeometrySerializer<>(Geometry.class));
|
||||
module.addDeserializer(Geometry.class, new GeometryDeserializer<>(Geometry.class));
|
||||
|
||||
module.addSerializer(Polygon.class, new GeometrySerializer<>(Polygon.class));
|
||||
module.addDeserializer(Polygon.class, new GeometryDeserializer<>(Polygon.class));
|
||||
|
||||
module.addSerializer(Point.class, new GeometrySerializer<>(Point.class));
|
||||
module.addDeserializer(Point.class, new GeometryDeserializer<>(Point.class));
|
||||
|
||||
return Jackson2ObjectMapperBuilder.json().modulesToInstall(module).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.kamco.cd.training.config.api;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper;
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.kamco.cd.training.config.api;
|
||||
|
||||
import com.kamco.cd.training.log.dto.EventStatus;
|
||||
import com.kamco.cd.training.log.dto.EventType;
|
||||
import com.kamco.cd.training.menu.dto.MenuDto;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public static String getUriMenuInfo(List<MenuDto.Basic> menuList, String uri) {
|
||||
|
||||
MenuDto.Basic m =
|
||||
menuList.stream()
|
||||
.filter(menu -> menu.getMenuApiUrl() != null && uri.contains(menu.getMenuApiUrl()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
return m != null ? m.getMenuUid() : "SYSTEM";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.kamco.cd.training.config.api;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.kamco.cd.training.auth.CustomUserDetails;
|
||||
import com.kamco.cd.training.menu.service.MenuService;
|
||||
import com.kamco.cd.training.postgres.entity.AuditLogEntity;
|
||||
import com.kamco.cd.training.postgres.repository.log.AuditLogRepository;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
||||
|
||||
/**
|
||||
* ApiResponseDto의 내장된 HTTP 상태 코드를 실제 HTTP 응답에 적용하는 Advice
|
||||
*
|
||||
* <p>createOK() → 201 CREATED ok() → 200 OK deleteOk() → 204 NO_CONTENT
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
public class ApiResponseAdvice implements ResponseBodyAdvice<Object> {
|
||||
|
||||
private final AuditLogRepository auditLogRepository;
|
||||
private final MenuService menuService;
|
||||
|
||||
@Autowired private ObjectMapper objectMapper;
|
||||
|
||||
public ApiResponseAdvice(AuditLogRepository auditLogRepository, MenuService menuService) {
|
||||
this.auditLogRepository = auditLogRepository;
|
||||
this.menuService = menuService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(
|
||||
MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
// ApiResponseDto를 반환하는 경우에만 적용
|
||||
return returnType.getParameterType().equals(ApiResponseDto.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object beforeBodyWrite(
|
||||
Object body,
|
||||
MethodParameter returnType,
|
||||
MediaType selectedContentType,
|
||||
Class<? extends HttpMessageConverter<?>> selectedConverterType,
|
||||
ServerHttpRequest request,
|
||||
ServerHttpResponse response) {
|
||||
|
||||
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
|
||||
ContentCachingRequestWrapper contentWrapper = null;
|
||||
if (servletRequest instanceof ContentCachingRequestWrapper wrapper) {
|
||||
contentWrapper = wrapper;
|
||||
}
|
||||
|
||||
if (body instanceof ApiResponseDto<?> apiResponse) {
|
||||
response.setStatusCode(apiResponse.getHttpStatus());
|
||||
|
||||
String ip = ApiLogFunction.getClientIp(servletRequest);
|
||||
Long userid = null;
|
||||
|
||||
if (servletRequest.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth
|
||||
&& auth.getPrincipal() instanceof CustomUserDetails customUserDetails) {
|
||||
userid = customUserDetails.getMember().getId();
|
||||
}
|
||||
|
||||
String requestBody;
|
||||
// 멀티파트 요청은 바디 로깅을 생략 (파일 바이너리로 인한 문제 예방)
|
||||
MediaType reqContentType = null;
|
||||
try {
|
||||
String ct = servletRequest.getContentType();
|
||||
reqContentType = ct != null ? MediaType.valueOf(ct) : null;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
if (reqContentType != null && MediaType.MULTIPART_FORM_DATA.includes(reqContentType)) {
|
||||
requestBody = "(multipart omitted)";
|
||||
} else {
|
||||
requestBody = ApiLogFunction.getRequestBody(servletRequest, contentWrapper);
|
||||
requestBody = maskSensitiveFields(requestBody);
|
||||
}
|
||||
|
||||
AuditLogEntity log =
|
||||
new AuditLogEntity(
|
||||
userid,
|
||||
ApiLogFunction.getEventType(servletRequest),
|
||||
ApiLogFunction.isSuccessFail(apiResponse),
|
||||
ApiLogFunction.getUriMenuInfo(
|
||||
menuService.getFindAll(), servletRequest.getRequestURI()),
|
||||
ip,
|
||||
servletRequest.getRequestURI(),
|
||||
requestBody,
|
||||
apiResponse.getErrorLogUid());
|
||||
auditLogRepository.save(log);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* 마스킹
|
||||
*
|
||||
* @param json
|
||||
* @return
|
||||
*/
|
||||
private String maskSensitiveFields(String json) {
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// password 마스킹
|
||||
json = json.replaceAll("\"password\"\\s*:\\s*\"[^\"]*\"", "\"password\":\"****\"");
|
||||
|
||||
// 토큰 마스킹
|
||||
json = json.replaceAll("\"accessToken\"\\s*:\\s*\"[^\"]*\"", "\"accessToken\":\"****\"");
|
||||
json = json.replaceAll("\"refreshToken\"\\s*:\\s*\"[^\"]*\"", "\"refreshToken\":\"****\"");
|
||||
|
||||
return json;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package com.kamco.cd.training.config.api;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public class ApiResponseDto<T> {
|
||||
|
||||
private T data;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
private Error error;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
private T errorData;
|
||||
|
||||
@JsonIgnore private HttpStatus httpStatus;
|
||||
|
||||
@JsonIgnore private Long errorLogUid;
|
||||
|
||||
public ApiResponseDto(T data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
private ApiResponseDto(T data, HttpStatus httpStatus) {
|
||||
this.data = data;
|
||||
this.httpStatus = httpStatus;
|
||||
}
|
||||
|
||||
public ApiResponseDto(ApiResponseCode code) {
|
||||
this.error = new Error(code.getId(), code.getMessage());
|
||||
}
|
||||
|
||||
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);
|
||||
this.errorData = errorData;
|
||||
}
|
||||
|
||||
// HTTP 상태 코드가 내장된 ApiResponseDto 반환 메서드들
|
||||
public static <T> ApiResponseDto<T> createOK(T data) {
|
||||
return new ApiResponseDto<>(data, HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
public static <T> ApiResponseDto<T> ok(T data) {
|
||||
return new ApiResponseDto<>(data, HttpStatus.OK);
|
||||
}
|
||||
|
||||
public static <T> ApiResponseDto<ResponseObj> okObject(ResponseObj data) {
|
||||
if (data.getCode().equals(ApiResponseCode.OK)) {
|
||||
return new ApiResponseDto<>(data, HttpStatus.NO_CONTENT);
|
||||
} else {
|
||||
return new ApiResponseDto<>(data.getCode(), data.getMessage(), HttpStatus.CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> ApiResponseDto<T> deleteOk(T data) {
|
||||
return new ApiResponseDto<>(data, HttpStatus.NO_CONTENT);
|
||||
}
|
||||
|
||||
public static ApiResponseDto<String> createException(ApiResponseCode code) {
|
||||
return new ApiResponseDto<>(code);
|
||||
}
|
||||
|
||||
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) {
|
||||
return new ApiResponseDto<>(code, message, data);
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static class Error {
|
||||
|
||||
private final String code;
|
||||
private final String message;
|
||||
|
||||
public Error(String code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
/** Error가 아닌 Business상 성공이거나 실패인 경우, 메세지 함께 전달하기 위한 object */
|
||||
@Getter
|
||||
public static class ResponseObj {
|
||||
|
||||
private final ApiResponseCode code;
|
||||
private final String message;
|
||||
|
||||
public ResponseObj(ApiResponseCode code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum ApiResponseCode implements EnumType {
|
||||
|
||||
// @formatter:off
|
||||
OK("요청이 성공하였습니다."),
|
||||
BAD_REQUEST("요청 파라미터가 잘못되었습니다."),
|
||||
BAD_GATEWAY("네트워크 상태가 불안정합니다."),
|
||||
ALREADY_EXIST_MALL("이미 등록된 쇼핑센터입니다."),
|
||||
NOT_FOUND_MAP("지도를 찾을 수 없습니다."),
|
||||
UNAUTHORIZED("권한이 없습니다."),
|
||||
CONFLICT("이미 등록된 컨텐츠입니다."),
|
||||
NOT_FOUND("Resource를 찾을 수 없습니다."),
|
||||
NOT_FOUND_DATA("데이터를 찾을 수 없습니다."),
|
||||
NOT_FOUND_WEATHER_DATA("날씨 데이터를 찾을 수 없습니다."),
|
||||
FAIL_SEND_MESSAGE("메시지를 전송하지 못했습니다."),
|
||||
TOO_MANY_CONNECTED_MACHINES("연결된 기기가 너무 많습니다."),
|
||||
UNAUTHENTICATED("인증에 실패하였습니다."),
|
||||
INVALID_TOKEN("잘못된 토큰입니다."),
|
||||
EXPIRED_TOKEN("만료된 토큰입니다."),
|
||||
INTERNAL_SERVER_ERROR("서버에 문제가 발생 하였습니다."),
|
||||
FORBIDDEN("권한을 확인해주세요."),
|
||||
INVALID_PASSWORD("잘못된 비밀번호 입니다."),
|
||||
NOT_FOUND_CAR_IN("입차정보가 없습니다."),
|
||||
WRONG_STATUS("잘못된 상태입니다."),
|
||||
FAIL_VERIFICATION("인증에 실패하였습니다."),
|
||||
INVALID_EMAIL("잘못된 형식의 이메일입니다."),
|
||||
REQUIRED_EMAIL("이메일은 필수 항목입니다."),
|
||||
WRONG_PASSWORD("잘못된 패스워드입니다."),
|
||||
DUPLICATE_EMAIL("이미 가입된 이메일입니다."),
|
||||
DUPLICATE_DATA("이미 등록되어 있습니다."),
|
||||
DATA_INTEGRITY_ERROR("데이터 무결성이 위반되어 요청을 처리할수 없습니다."),
|
||||
FOREIGN_KEY_ERROR("참조 중인 데이터가 있어 삭제할 수 없습니다."),
|
||||
DUPLICATE_EMPLOYEEID("이미 가입된 사번입니다."),
|
||||
NOT_FOUND_USER_FOR_EMAIL("이메일로 유저를 찾을 수 없습니다."),
|
||||
NOT_FOUND_USER("사용자를 찾을 수 없습니다."),
|
||||
UNPROCESSABLE_ENTITY("이 데이터는 삭제할 수 없습니다."),
|
||||
LOGIN_ID_NOT_FOUND("아이디를 잘못 입력하셨습니다."),
|
||||
LOGIN_PASSWORD_MISMATCH("비밀번호를 잘못 입력하셨습니다."),
|
||||
LOGIN_PASSWORD_EXCEEDED("비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다.\n로그인 오류에 대해 관리자에게 문의하시기 바랍니다."),
|
||||
REFRESH_TOKEN_EXPIRED_OR_REVOKED("토큰 정보가 만료 되었습니다."),
|
||||
REFRESH_TOKEN_MISMATCH("토큰 정보가 일치하지 않습니다."),
|
||||
INACTIVE_ID("사용할 수 없는 계정입니다."),
|
||||
INVALID_EMAIL_TOKEN(
|
||||
"You can only reset your password within 24 hours from when the email was sent.\n"
|
||||
+ "To reset your password again, please submit a new request through \"Forgot"
|
||||
+ " Password.\""),
|
||||
;
|
||||
// @formatter:on
|
||||
private final String message;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user