add common module

This commit is contained in:
cherry2250
2025-10-23 17:54:28 +09:00
parent 8029d8f9ce
commit ea82ff4748
57 changed files with 4706 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
plugins {
id 'java-library'
id 'org.springframework.boot'
id 'io.spring.dependency-management'
}
// common 모듈은 실행 가능한 jar가 아니므로 bootJar 비활성화
bootJar {
enabled = false
}
jar {
enabled = true
}
dependencies {
// Spring Boot Starters
api 'org.springframework.boot:spring-boot-starter-web'
api 'org.springframework.boot:spring-boot-starter-security'
api 'org.springframework.boot:spring-boot-starter-data-jpa'
api 'org.springframework.boot:spring-boot-starter-validation'
// JWT
api "io.jsonwebtoken:jjwt-api:${jjwtVersion}"
runtimeOnly "io.jsonwebtoken:jjwt-impl:${jjwtVersion}"
runtimeOnly "io.jsonwebtoken:jjwt-jackson:${jjwtVersion}"
// Utilities
api "org.apache.commons:commons-lang3:${commonsLang3Version}"
api "commons-io:commons-io:${commonsIoVersion}"
// Jackson for JSON
api 'com.fasterxml.jackson.core:jackson-databind'
api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
}
@@ -0,0 +1,78 @@
package com.kt.event.common.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.time.LocalDateTime;
/**
* 공통 API 응답 래퍼
* 모든 API 응답을 감싸는 표준 응답 포맷
*
* @param <T> 응답 데이터 타입
*/
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiResponse<T> {
/**
* 성공 여부
*/
private final boolean success;
/**
* 응답 데이터
*/
private final T data;
/**
* 에러 코드 (실패 시)
*/
private final String errorCode;
/**
* 에러 메시지 (실패 시)
*/
private final String message;
/**
* 응답 시간
*/
private final LocalDateTime timestamp;
/**
* 성공 응답 생성 (데이터 포함)
*
* @param data 응답 데이터
* @param <T> 응답 데이터 타입
* @return API 응답
*/
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, data, null, null, LocalDateTime.now());
}
/**
* 성공 응답 생성 (데이터 없음)
*
* @param <T> 응답 데이터 타입
* @return API 응답
*/
public static <T> ApiResponse<T> success() {
return new ApiResponse<>(true, null, null, null, LocalDateTime.now());
}
/**
* 실패 응답 생성
*
* @param errorCode 에러 코드
* @param message 에러 메시지
* @param <T> 응답 데이터 타입
* @return API 응답
*/
public static <T> ApiResponse<T> error(String errorCode, String message) {
return new ApiResponse<>(false, null, errorCode, message, LocalDateTime.now());
}
}
@@ -0,0 +1,122 @@
package com.kt.event.common.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import java.time.LocalDateTime;
import java.util.List;
/**
* 에러 응답
* API 에러 발생 시 반환되는 상세 에러 정보
*/
@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ErrorResponse {
/**
* 성공 여부 (항상 false)
*/
@Builder.Default
private final boolean success = false;
/**
* 에러 코드
*/
private final String errorCode;
/**
* 에러 메시지
*/
private final String message;
/**
* 상세 에러 정보 (선택)
*/
private final String details;
/**
* 유효성 검증 에러 목록 (선택)
*/
private final List<FieldError> fieldErrors;
/**
* 에러 발생 시간
*/
@Builder.Default
private final LocalDateTime timestamp = LocalDateTime.now();
/**
* 필드 유효성 검증 에러
*/
@Getter
@Builder
@AllArgsConstructor
public static class FieldError {
/**
* 필드명
*/
private final String field;
/**
* 입력된 값
*/
private final Object rejectedValue;
/**
* 에러 메시지
*/
private final String message;
}
/**
* 기본 에러 응답 생성
*
* @param errorCode 에러 코드
* @param message 에러 메시지
* @return ErrorResponse
*/
public static ErrorResponse of(String errorCode, String message) {
return ErrorResponse.builder()
.errorCode(errorCode)
.message(message)
.build();
}
/**
* 상세 정보가 포함된 에러 응답 생성
*
* @param errorCode 에러 코드
* @param message 에러 메시지
* @param details 상세 에러 정보
* @return ErrorResponse
*/
public static ErrorResponse of(String errorCode, String message, String details) {
return ErrorResponse.builder()
.errorCode(errorCode)
.message(message)
.details(details)
.build();
}
/**
* 필드 유효성 검증 에러 응답 생성
*
* @param errorCode 에러 코드
* @param message 에러 메시지
* @param fieldErrors 필드 에러 목록
* @return ErrorResponse
*/
public static ErrorResponse of(String errorCode, String message, List<FieldError> fieldErrors) {
return ErrorResponse.builder()
.errorCode(errorCode)
.message(message)
.fieldErrors(fieldErrors)
.build();
}
}
@@ -0,0 +1,75 @@
package com.kt.event.common.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 페이지네이션 응답
* 목록 조회 시 페이징 정보를 포함하는 응답
*
* @param <T> 목록 아이템 타입
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PageResponse<T> {
/**
* 목록 데이터
*/
private List<T> content;
/**
* 현재 페이지 번호 (0부터 시작)
*/
private int page;
/**
* 페이지 크기
*/
private int size;
/**
* 전체 요소 수
*/
private long totalElements;
/**
* 전체 페이지 수
*/
private int totalPages;
/**
* 첫 페이지 여부
*/
private boolean first;
/**
* 마지막 페이지 여부
*/
private boolean last;
/**
* Spring Data Page를 PageResponse로 변환
*
* @param page Spring Data Page 객체
* @param <T> 목록 아이템 타입
* @return PageResponse
*/
public static <T> PageResponse<T> of(org.springframework.data.domain.Page<T> page) {
return PageResponse.<T>builder()
.content(page.getContent())
.page(page.getNumber())
.size(page.getSize())
.totalElements(page.getTotalElements())
.totalPages(page.getTotalPages())
.first(page.isFirst())
.last(page.isLast())
.build();
}
}
@@ -0,0 +1,35 @@
package com.kt.event.common.entity;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
/**
* 베이스 타임 엔티티
* 생성일시, 수정일시를 자동으로 관리하는 공통 엔티티
*/
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
/**
* 생성일시
*/
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
/**
* 수정일시
*/
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
}
@@ -0,0 +1,84 @@
package com.kt.event.common.exception;
import lombok.Getter;
/**
* 비즈니스 예외
* 비즈니스 로직 처리 중 발생하는 예외
* (예: 중복 데이터, 권한 없음, 유효하지 않은 상태 전환 등)
*/
@Getter
public class BusinessException extends RuntimeException {
/**
* 에러 코드
*/
private final ErrorCode errorCode;
/**
* 상세 에러 정보
*/
private final String details;
/**
* 비즈니스 예외 생성 (기본 메시지 사용)
*
* @param errorCode 에러 코드
*/
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
this.details = null;
}
/**
* 비즈니스 예외 생성 (커스텀 메시지 사용)
*
* @param errorCode 에러 코드
* @param message 커스텀 에러 메시지
*/
public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
this.details = null;
}
/**
* 비즈니스 예외 생성 (상세 정보 포함)
*
* @param errorCode 에러 코드
* @param message 커스텀 에러 메시지
* @param details 상세 에러 정보
*/
public BusinessException(ErrorCode errorCode, String message, String details) {
super(message);
this.errorCode = errorCode;
this.details = details;
}
/**
* 비즈니스 예외 생성 (원인 예외 포함)
*
* @param errorCode 에러 코드
* @param cause 원인 예외
*/
public BusinessException(ErrorCode errorCode, Throwable cause) {
super(errorCode.getMessage(), cause);
this.errorCode = errorCode;
this.details = cause.getMessage();
}
/**
* 비즈니스 예외 생성 (모든 정보 포함)
*
* @param errorCode 에러 코드
* @param message 커스텀 에러 메시지
* @param details 상세 에러 정보
* @param cause 원인 예외
*/
public BusinessException(ErrorCode errorCode, String message, String details, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
this.details = details;
}
}
@@ -0,0 +1,108 @@
package com.kt.event.common.exception;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* 에러 코드 정의
* 시스템 전체에서 사용하는 에러 코드와 메시지를 관리
*/
@Getter
@RequiredArgsConstructor
public enum ErrorCode {
// 공통 에러 (COMMON_XXX)
COMMON_001("COMMON_001", "잘못된 요청입니다"),
COMMON_002("COMMON_002", "필수 파라미터가 누락되었습니다"),
COMMON_003("COMMON_003", "유효성 검증에 실패했습니다"),
COMMON_004("COMMON_004", "서버 내부 오류가 발생했습니다"),
COMMON_005("COMMON_005", "지원하지 않는 작업입니다"),
// 인증/인가 에러 (AUTH_XXX)
AUTH_001("AUTH_001", "인증에 실패했습니다"),
AUTH_002("AUTH_002", "유효하지 않은 토큰입니다"),
AUTH_003("AUTH_003", "만료된 토큰입니다"),
AUTH_004("AUTH_004", "권한이 없습니다"),
AUTH_005("AUTH_005", "토큰이 제공되지 않았습니다"),
// 사용자 에러 (USER_XXX)
USER_001("USER_001", "이미 존재하는 사용자입니다"),
USER_002("USER_002", "사업자번호 검증에 실패했습니다"),
USER_003("USER_003", "사용자를 찾을 수 없습니다"),
USER_004("USER_004", "비밀번호가 일치하지 않습니다"),
USER_005("USER_005", "휴폐업 사업자번호입니다"),
// 이벤트 에러 (EVENT_XXX)
EVENT_001("EVENT_001", "이벤트를 찾을 수 없습니다"),
EVENT_002("EVENT_002", "유효하지 않은 상태 전환입니다"),
EVENT_003("EVENT_003", "필수 데이터가 누락되었습니다"),
EVENT_004("EVENT_004", "이벤트 생성에 실패했습니다"),
EVENT_005("EVENT_005", "이벤트 수정 권한이 없습니다"),
// Job 에러 (JOB_XXX)
JOB_001("JOB_001", "Job을 찾을 수 없습니다"),
JOB_002("JOB_002", "Job 처리에 실패했습니다"),
JOB_003("JOB_003", "Job이 아직 처리 중입니다"),
JOB_004("JOB_004", "Job 타임아웃이 발생했습니다"),
// AI 에러 (AI_XXX)
AI_001("AI_001", "AI 추천 생성에 실패했습니다"),
AI_002("AI_002", "트렌드 분석에 실패했습니다"),
AI_003("AI_003", "AI API 호출에 실패했습니다"),
AI_004("AI_004", "AI 추천 결과를 찾을 수 없습니다"),
// 콘텐츠 에러 (CONTENT_XXX)
CONTENT_001("CONTENT_001", "이미지 생성에 실패했습니다"),
CONTENT_002("CONTENT_002", "이미지를 찾을 수 없습니다"),
CONTENT_003("CONTENT_003", "CDN 업로드에 실패했습니다"),
CONTENT_004("CONTENT_004", "콘텐츠를 찾을 수 없습니다"),
// 배포 에러 (DIST_XXX)
DIST_001("DIST_001", "배포에 실패했습니다"),
DIST_002("DIST_002", "채널 연동에 실패했습니다"),
DIST_003("DIST_003", "서킷 브레이커가 열려있습니다"),
DIST_004("DIST_004", "배포 상태를 찾을 수 없습니다"),
// 참여 에러 (PART_XXX)
PART_001("PART_001", "이미 참여한 이벤트입니다"),
PART_002("PART_002", "이벤트 참여 기간이 아닙니다"),
PART_003("PART_003", "참여자를 찾을 수 없습니다"),
PART_004("PART_004", "당첨자 추첨에 실패했습니다"),
PART_005("PART_005", "이벤트가 종료되었습니다"),
// 분석 에러 (ANALYTICS_XXX)
ANALYTICS_001("ANALYTICS_001", "분석 데이터를 찾을 수 없습니다"),
ANALYTICS_002("ANALYTICS_002", "외부 API 호출에 실패했습니다"),
ANALYTICS_003("ANALYTICS_003", "통계 계산에 실패했습니다"),
// 외부 연동 에러 (EXTERNAL_XXX)
EXTERNAL_001("EXTERNAL_001", "외부 API 호출에 실패했습니다"),
EXTERNAL_002("EXTERNAL_002", "외부 API 타임아웃이 발생했습니다"),
EXTERNAL_003("EXTERNAL_003", "외부 API 응답 형식이 올바르지 않습니다"),
// 데이터베이스 에러 (DB_XXX)
DB_001("DB_001", "데이터베이스 연결에 실패했습니다"),
DB_002("DB_002", "데이터 저장에 실패했습니다"),
DB_003("DB_003", "데이터 조회에 실패했습니다"),
DB_004("DB_004", "데이터 삭제에 실패했습니다"),
// Redis 에러 (REDIS_XXX)
REDIS_001("REDIS_001", "Redis 연결에 실패했습니다"),
REDIS_002("REDIS_002", "캐시 저장에 실패했습니다"),
REDIS_003("REDIS_003", "캐시 조회에 실패했습니다"),
// Kafka 에러 (KAFKA_XXX)
KAFKA_001("KAFKA_001", "Kafka 메시지 발행에 실패했습니다"),
KAFKA_002("KAFKA_002", "Kafka 메시지 소비에 실패했습니다"),
KAFKA_003("KAFKA_003", "Kafka 연결에 실패했습니다");
/**
* 에러 코드
*/
private final String code;
/**
* 에러 메시지
*/
private final String message;
}
@@ -0,0 +1,198 @@
package com.kt.event.common.exception;
import com.kt.event.common.dto.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;
import java.util.stream.Collectors;
/**
* 전역 예외 핸들러
* 애플리케이션 전체에서 발생하는 예외를 일관된 형식으로 처리
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 비즈니스 예외 처리
*
* @param ex 비즈니스 예외
* @return 에러 응답
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex) {
log.warn("Business exception occurred: {}", ex.getMessage());
ErrorResponse errorResponse = ErrorResponse.of(
ex.getErrorCode().getCode(),
ex.getMessage(),
ex.getDetails()
);
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(errorResponse);
}
/**
* 인프라 예외 처리
*
* @param ex 인프라 예외
* @return 에러 응답
*/
@ExceptionHandler(InfraException.class)
public ResponseEntity<ErrorResponse> handleInfraException(InfraException ex) {
log.error("Infrastructure exception occurred: {}", ex.getMessage(), ex);
ErrorResponse errorResponse = ErrorResponse.of(
ex.getErrorCode().getCode(),
ex.getMessage(),
ex.getDetails()
);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(errorResponse);
}
/**
* 인증 예외 처리
*
* @param ex 인증 예외
* @return 에러 응답
*/
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<ErrorResponse> handleAuthenticationException(AuthenticationException ex) {
log.warn("Authentication exception occurred: {}", ex.getMessage());
ErrorResponse errorResponse = ErrorResponse.of(
ErrorCode.AUTH_001.getCode(),
ErrorCode.AUTH_001.getMessage(),
ex.getMessage()
);
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(errorResponse);
}
/**
* 권한 예외 처리
*
* @param ex 권한 예외
* @return 에러 응답
*/
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorResponse> handleAccessDeniedException(AccessDeniedException ex) {
log.warn("Access denied exception occurred: {}", ex.getMessage());
ErrorResponse errorResponse = ErrorResponse.of(
ErrorCode.AUTH_004.getCode(),
ErrorCode.AUTH_004.getMessage(),
ex.getMessage()
);
return ResponseEntity
.status(HttpStatus.FORBIDDEN)
.body(errorResponse);
}
/**
* 유효성 검증 예외 처리 (RequestBody)
*
* @param ex 유효성 검증 예외
* @return 에러 응답
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
log.warn("Validation exception occurred: {}", ex.getMessage());
List<ErrorResponse.FieldError> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(this::mapToFieldError)
.collect(Collectors.toList());
ErrorResponse errorResponse = ErrorResponse.of(
ErrorCode.COMMON_003.getCode(),
ErrorCode.COMMON_003.getMessage(),
fieldErrors
);
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(errorResponse);
}
/**
* 유효성 검증 예외 처리 (ModelAttribute)
*
* @param ex 유효성 검증 예외
* @return 에러 응답
*/
@ExceptionHandler(BindException.class)
public ResponseEntity<ErrorResponse> handleBindException(BindException ex) {
log.warn("Bind exception occurred: {}", ex.getMessage());
List<ErrorResponse.FieldError> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(this::mapToFieldError)
.collect(Collectors.toList());
ErrorResponse errorResponse = ErrorResponse.of(
ErrorCode.COMMON_003.getCode(),
ErrorCode.COMMON_003.getMessage(),
fieldErrors
);
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(errorResponse);
}
/**
* 일반 예외 처리
*
* @param ex 일반 예외
* @return 에러 응답
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception ex) {
log.error("Unexpected exception occurred: {}", ex.getMessage(), ex);
ErrorResponse errorResponse = ErrorResponse.of(
ErrorCode.COMMON_004.getCode(),
ErrorCode.COMMON_004.getMessage(),
ex.getMessage()
);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(errorResponse);
}
/**
* Spring FieldError를 ErrorResponse.FieldError로 변환
*
* @param fieldError Spring FieldError
* @return ErrorResponse.FieldError
*/
private ErrorResponse.FieldError mapToFieldError(FieldError fieldError) {
return ErrorResponse.FieldError.builder()
.field(fieldError.getField())
.rejectedValue(fieldError.getRejectedValue())
.message(fieldError.getDefaultMessage())
.build();
}
}
@@ -0,0 +1,84 @@
package com.kt.event.common.exception;
import lombok.Getter;
/**
* 인프라 예외
* 인프라 계층에서 발생하는 예외
* (예: DB 연결 실패, Redis 오류, Kafka 오류, 외부 API 호출 실패 등)
*/
@Getter
public class InfraException extends RuntimeException {
/**
* 에러 코드
*/
private final ErrorCode errorCode;
/**
* 상세 에러 정보
*/
private final String details;
/**
* 인프라 예외 생성 (기본 메시지 사용)
*
* @param errorCode 에러 코드
*/
public InfraException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
this.details = null;
}
/**
* 인프라 예외 생성 (커스텀 메시지 사용)
*
* @param errorCode 에러 코드
* @param message 커스텀 에러 메시지
*/
public InfraException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
this.details = null;
}
/**
* 인프라 예외 생성 (상세 정보 포함)
*
* @param errorCode 에러 코드
* @param message 커스텀 에러 메시지
* @param details 상세 에러 정보
*/
public InfraException(ErrorCode errorCode, String message, String details) {
super(message);
this.errorCode = errorCode;
this.details = details;
}
/**
* 인프라 예외 생성 (원인 예외 포함)
*
* @param errorCode 에러 코드
* @param cause 원인 예외
*/
public InfraException(ErrorCode errorCode, Throwable cause) {
super(errorCode.getMessage(), cause);
this.errorCode = errorCode;
this.details = cause.getMessage();
}
/**
* 인프라 예외 생성 (모든 정보 포함)
*
* @param errorCode 에러 코드
* @param message 커스텀 에러 메시지
* @param details 상세 에러 정보
* @param cause 원인 예외
*/
public InfraException(ErrorCode errorCode, String message, String details, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
this.details = details;
}
}
@@ -0,0 +1,130 @@
package com.kt.event.common.security;
import com.kt.event.common.util.StringUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* JWT 인증 필터
* 요청 헤더에서 JWT 토큰을 추출하고 인증 처리
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
/**
* Authorization 헤더 이름
*/
private static final String AUTHORIZATION_HEADER = "Authorization";
/**
* Bearer 토큰 접두사
*/
private static final String BEARER_PREFIX = "Bearer ";
/**
* JWT 토큰 제공자
*/
private final JwtTokenProvider jwtTokenProvider;
/**
* 필터 실행
*
* @param request HTTP 요청
* @param response HTTP 응답
* @param filterChain 필터 체인
* @throws ServletException 서블릿 예외
* @throws IOException 입출력 예외
*/
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
// 요청에서 JWT 토큰 추출
String token = extractTokenFromRequest(request);
// 토큰이 존재하고 유효한 경우 인증 처리
if (StringUtil.isNotBlank(token) && jwtTokenProvider.validateToken(token)) {
// Access Token인지 확인
if (jwtTokenProvider.isAccessToken(token)) {
authenticateUser(token, request);
} else {
log.warn("Refresh token used for authentication: {}", request.getRequestURI());
}
}
} catch (Exception e) {
log.error("Could not set user authentication in security context", e);
}
filterChain.doFilter(request, response);
}
/**
* 요청에서 JWT 토큰 추출
*
* @param request HTTP 요청
* @return JWT 토큰 (없으면 null)
*/
private String extractTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtil.isNotBlank(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(BEARER_PREFIX.length());
}
return null;
}
/**
* 사용자 인증 처리
*
* @param token JWT 토큰
* @param request HTTP 요청
*/
private void authenticateUser(String token, HttpServletRequest request) {
// 토큰에서 사용자 정보 추출
UserPrincipal userPrincipal = jwtTokenProvider.getUserPrincipalFromToken(token);
// Spring Security 인증 객체 생성
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userPrincipal,
null,
userPrincipal.getAuthorities()
);
// 요청 상세 정보 설정
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// SecurityContext에 인증 정보 저장
SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("Set authentication for user: {} (userId: {})",
userPrincipal.getEmail(), userPrincipal.getUserId());
}
/**
* 필터 적용 여부 결정
* OPTIONS 요청은 필터를 적용하지 않음
*
* @param request HTTP 요청
* @return 필터 적용 제외 여부
*/
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return "OPTIONS".equalsIgnoreCase(request.getMethod());
}
}
@@ -0,0 +1,215 @@
package com.kt.event.common.security;
import com.kt.event.common.exception.ErrorCode;
import com.kt.event.common.exception.InfraException;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.List;
/**
* JWT 토큰 생성 및 검증 제공자
* Access Token 및 Refresh Token 생성/검증 기능 제공
*/
@Slf4j
@Component
public class JwtTokenProvider {
/**
* JWT 서명 키
*/
private final SecretKey secretKey;
/**
* Access Token 유효기간 (밀리초)
*/
private final long accessTokenValidityMs;
/**
* Refresh Token 유효기간 (밀리초)
*/
private final long refreshTokenValidityMs;
public JwtTokenProvider(
@Value("${jwt.secret}") String secret,
@Value("${jwt.access-token-validity:3600000}") long accessTokenValidityMs,
@Value("${jwt.refresh-token-validity:604800000}") long refreshTokenValidityMs) {
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
this.accessTokenValidityMs = accessTokenValidityMs;
this.refreshTokenValidityMs = refreshTokenValidityMs;
}
/**
* Access Token 생성
*
* @param userId 사용자 ID
* @param email 이메일
* @param name 이름
* @param roles 역할 목록
* @return Access Token
*/
public String createAccessToken(Long userId, String email, String name, List<String> roles) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + accessTokenValidityMs);
return Jwts.builder()
.subject(userId.toString())
.claim("email", email)
.claim("name", name)
.claim("roles", roles)
.claim("type", "access")
.issuedAt(now)
.expiration(expiryDate)
.signWith(secretKey)
.compact();
}
/**
* Refresh Token 생성
*
* @param userId 사용자 ID
* @return Refresh Token
*/
public String createRefreshToken(Long userId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + refreshTokenValidityMs);
return Jwts.builder()
.subject(userId.toString())
.claim("type", "refresh")
.issuedAt(now)
.expiration(expiryDate)
.signWith(secretKey)
.compact();
}
/**
* 토큰에서 사용자 ID 추출
*
* @param token JWT 토큰
* @return 사용자 ID
*/
public Long getUserIdFromToken(String token) {
Claims claims = parseToken(token);
return Long.parseLong(claims.getSubject());
}
/**
* 토큰에서 UserPrincipal 추출
*
* @param token JWT 토큰
* @return UserPrincipal
*/
public UserPrincipal getUserPrincipalFromToken(String token) {
Claims claims = parseToken(token);
Long userId = Long.parseLong(claims.getSubject());
String email = claims.get("email", String.class);
String name = claims.get("name", String.class);
@SuppressWarnings("unchecked")
List<String> roles = claims.get("roles", List.class);
return new UserPrincipal(userId, email, name, roles);
}
/**
* 토큰 유효성 검증
*
* @param token JWT 토큰
* @return 유효 여부
*/
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
log.error("Invalid JWT signature: {}", e.getMessage());
} catch (ExpiredJwtException e) {
log.error("Expired JWT token: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
log.error("Unsupported JWT token: {}", e.getMessage());
} catch (IllegalArgumentException e) {
log.error("JWT claims string is empty: {}", e.getMessage());
}
return false;
}
/**
* 토큰 타입 확인 (access/refresh)
*
* @param token JWT 토큰
* @return 토큰 타입
*/
public String getTokenType(String token) {
Claims claims = parseToken(token);
return claims.get("type", String.class);
}
/**
* Access Token 여부 확인
*
* @param token JWT 토큰
* @return Access Token 여부
*/
public boolean isAccessToken(String token) {
return "access".equals(getTokenType(token));
}
/**
* Refresh Token 여부 확인
*
* @param token JWT 토큰
* @return Refresh Token 여부
*/
public boolean isRefreshToken(String token) {
return "refresh".equals(getTokenType(token));
}
/**
* 토큰 파싱
*
* @param token JWT 토큰
* @return Claims
*/
private Claims parseToken(String token) {
try {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
} catch (ExpiredJwtException e) {
throw new InfraException(ErrorCode.AUTH_002, e);
} catch (Exception e) {
throw new InfraException(ErrorCode.AUTH_003, e);
}
}
/**
* 토큰 만료 시간 조회
*
* @param token JWT 토큰
* @return 만료 시간
*/
public Date getExpirationFromToken(String token) {
Claims claims = parseToken(token);
return claims.getExpiration();
}
/**
* 토큰 발급 시간 조회
*
* @param token JWT 토큰
* @return 발급 시간
*/
public Date getIssuedAtFromToken(String token) {
Claims claims = parseToken(token);
return claims.getIssuedAt();
}
}
@@ -0,0 +1,122 @@
package com.kt.event.common.security;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* Spring Security 인증 주체
* JWT 토큰에서 추출한 사용자 정보를 담는 객체
*/
@Getter
@AllArgsConstructor
public class UserPrincipal implements UserDetails {
/**
* 사용자 ID
*/
private final Long userId;
/**
* 사용자 이메일
*/
private final String email;
/**
* 사용자 이름
*/
private final String name;
/**
* 사용자 역할 목록
*/
private final List<String> roles;
/**
* Spring Security 권한 목록 반환
*
* @return 권한 목록
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
/**
* 비밀번호 반환 (JWT 인증에서는 사용하지 않음)
*
* @return null
*/
@Override
public String getPassword() {
return null;
}
/**
* 사용자명 반환 (이메일 사용)
*
* @return 이메일
*/
@Override
public String getUsername() {
return email;
}
/**
* 계정 만료 여부
*
* @return true (만료되지 않음)
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 계정 잠김 여부
*
* @return true (잠기지 않음)
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 자격증명 만료 여부
*
* @return true (만료되지 않음)
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 계정 활성화 여부
*
* @return true (활성화됨)
*/
@Override
public boolean isEnabled() {
return true;
}
/**
* 특정 역할 보유 여부 확인
*
* @param role 역할명
* @return 역할 보유 여부
*/
public boolean hasRole(String role) {
return roles.contains(role);
}
}
@@ -0,0 +1,148 @@
package com.kt.event.common.util;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
/**
* 날짜/시간 유틸리티
* 날짜와 시간 관련 공통 기능 제공
*/
public class DateTimeUtil {
/**
* 기본 날짜 시간 포맷터 (yyyy-MM-dd HH:mm:ss)
*/
private static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
* 날짜 포맷터 (yyyy-MM-dd)
*/
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
/**
* 시간 포맷터 (HH:mm:ss)
*/
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
/**
* 기본 타임존 (Asia/Seoul)
*/
private static final ZoneId DEFAULT_ZONE = ZoneId.of("Asia/Seoul");
/**
* 현재 시간 조회 (Asia/Seoul 타임존)
*
* @return 현재 시간
*/
public static LocalDateTime now() {
return LocalDateTime.now(DEFAULT_ZONE);
}
/**
* LocalDateTime을 기본 포맷 문자열로 변환
*
* @param dateTime LocalDateTime 객체
* @return 포맷된 문자열 (yyyy-MM-dd HH:mm:ss)
*/
public static String format(LocalDateTime dateTime) {
if (dateTime == null) {
return null;
}
return dateTime.format(DEFAULT_FORMATTER);
}
/**
* LocalDateTime을 날짜 포맷 문자열로 변환
*
* @param dateTime LocalDateTime 객체
* @return 포맷된 문자열 (yyyy-MM-dd)
*/
public static String formatDate(LocalDateTime dateTime) {
if (dateTime == null) {
return null;
}
return dateTime.format(DATE_FORMATTER);
}
/**
* LocalDateTime을 시간 포맷 문자열로 변환
*
* @param dateTime LocalDateTime 객체
* @return 포맷된 문자열 (HH:mm:ss)
*/
public static String formatTime(LocalDateTime dateTime) {
if (dateTime == null) {
return null;
}
return dateTime.format(TIME_FORMATTER);
}
/**
* 문자열을 LocalDateTime으로 파싱
*
* @param dateTimeStr 날짜 시간 문자열 (yyyy-MM-dd HH:mm:ss)
* @return LocalDateTime 객체
*/
public static LocalDateTime parse(String dateTimeStr) {
if (dateTimeStr == null || dateTimeStr.isEmpty()) {
return null;
}
return LocalDateTime.parse(dateTimeStr, DEFAULT_FORMATTER);
}
/**
* 두 날짜 사이의 차이 계산 (일 단위)
*
* @param start 시작 날짜
* @param end 종료 날짜
* @return 일수 차이
*/
public static long daysBetween(LocalDateTime start, LocalDateTime end) {
if (start == null || end == null) {
return 0;
}
return java.time.Duration.between(start, end).toDays();
}
/**
* 날짜가 특정 범위 내에 있는지 확인
*
* @param target 확인할 날짜
* @param start 시작 날짜
* @param end 종료 날짜
* @return 범위 내 여부
*/
public static boolean isBetween(LocalDateTime target, LocalDateTime start, LocalDateTime end) {
if (target == null || start == null || end == null) {
return false;
}
return !target.isBefore(start) && !target.isAfter(end);
}
/**
* 날짜가 현재보다 이전인지 확인
*
* @param dateTime 확인할 날짜
* @return 과거 날짜 여부
*/
public static boolean isPast(LocalDateTime dateTime) {
if (dateTime == null) {
return false;
}
return dateTime.isBefore(now());
}
/**
* 날짜가 현재보다 이후인지 확인
*
* @param dateTime 확인할 날짜
* @return 미래 날짜 여부
*/
public static boolean isFuture(LocalDateTime dateTime) {
if (dateTime == null) {
return false;
}
return dateTime.isAfter(now());
}
}
@@ -0,0 +1,157 @@
package com.kt.event.common.util;
import com.kt.event.common.exception.InfraException;
import com.kt.event.common.exception.ErrorCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
/**
* 암호화 유틸리티
* 비밀번호 해싱 및 데이터 암호화 기능 제공
*/
@Slf4j
public class EncryptionUtil {
/**
* BCrypt 인코더 (Cost Factor: 10)
*/
private static final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(10);
/**
* AES-256-GCM 설정
*/
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_TAG_LENGTH = 128;
private static final int GCM_IV_LENGTH = 12;
/**
* 비밀번호 해싱 (BCrypt)
*
* @param rawPassword 원본 비밀번호
* @return 해싱된 비밀번호
*/
public static String hashPassword(String rawPassword) {
if (StringUtil.isBlank(rawPassword)) {
throw new InfraException(ErrorCode.COMMON_002, "비밀번호는 필수입니다");
}
return passwordEncoder.encode(rawPassword);
}
/**
* 비밀번호 검증 (BCrypt)
*
* @param rawPassword 원본 비밀번호
* @param hashedPassword 해싱된 비밀번호
* @return 일치 여부
*/
public static boolean verifyPassword(String rawPassword, String hashedPassword) {
if (StringUtil.isBlank(rawPassword) || StringUtil.isBlank(hashedPassword)) {
return false;
}
try {
return passwordEncoder.matches(rawPassword, hashedPassword);
} catch (Exception e) {
log.error("Password verification failed", e);
return false;
}
}
/**
* AES-256-GCM 암호화
*
* @param plainText 평문
* @param secretKey 비밀키 (32바이트)
* @return Base64 인코딩된 암호문
*/
public static String encrypt(String plainText, String secretKey) {
if (StringUtil.isBlank(plainText)) {
return plainText;
}
try {
// IV 생성 (12바이트)
byte[] iv = new byte[GCM_IV_LENGTH];
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
// 암호화
SecretKey key = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "AES");
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
// IV + 암호문 결합
ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + cipherText.length);
byteBuffer.put(iv);
byteBuffer.put(cipherText);
// Base64 인코딩
return Base64.getEncoder().encodeToString(byteBuffer.array());
} catch (Exception e) {
log.error("Encryption failed", e);
throw new InfraException(ErrorCode.COMMON_004, e);
}
}
/**
* AES-256-GCM 복호화
*
* @param cipherText Base64 인코딩된 암호문
* @param secretKey 비밀키 (32바이트)
* @return 평문
*/
public static String decrypt(String cipherText, String secretKey) {
if (StringUtil.isBlank(cipherText)) {
return cipherText;
}
try {
// Base64 디코딩
byte[] decodedData = Base64.getDecoder().decode(cipherText);
// IV와 암호문 분리
ByteBuffer byteBuffer = ByteBuffer.wrap(decodedData);
byte[] iv = new byte[GCM_IV_LENGTH];
byteBuffer.get(iv);
byte[] encrypted = new byte[byteBuffer.remaining()];
byteBuffer.get(encrypted);
// 복호화
SecretKey key = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "AES");
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec);
byte[] decryptedData = cipher.doFinal(encrypted);
return new String(decryptedData, StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("Decryption failed", e);
throw new InfraException(ErrorCode.COMMON_004, e);
}
}
/**
* 32바이트 비밀키 생성 (개발/테스트용)
* 실제 운영에서는 환경변수나 Key Management Service 사용 권장
*
* @param seed 시드 문자열
* @return 32바이트 비밀키
*/
public static String generateSecretKey(String seed) {
String paddedSeed = (seed + "00000000000000000000000000000000").substring(0, 32);
return paddedSeed;
}
}
@@ -0,0 +1,178 @@
package com.kt.event.common.util;
import org.apache.commons.lang3.StringUtils;
/**
* 문자열 유틸리티
* 문자열 처리 관련 공통 기능 제공
*/
public class StringUtil {
/**
* 문자열이 null이거나 공백인지 확인
*
* @param str 확인할 문자열
* @return null 또는 공백 여부
*/
public static boolean isBlank(String str) {
return StringUtils.isBlank(str);
}
/**
* 문자열이 null이 아니고 공백이 아닌지 확인
*
* @param str 확인할 문자열
* @return null 또는 공백이 아닌지 여부
*/
public static boolean isNotBlank(String str) {
return StringUtils.isNotBlank(str);
}
/**
* 전화번호 마스킹 처리
* 예: 010-1234-5678 → 010-****-5678
*
* @param phoneNumber 전화번호
* @return 마스킹된 전화번호
*/
public static String maskPhoneNumber(String phoneNumber) {
if (isBlank(phoneNumber)) {
return phoneNumber;
}
String cleaned = phoneNumber.replaceAll("[^0-9]", "");
if (cleaned.length() == 11) {
return cleaned.substring(0, 3) + "-****-" + cleaned.substring(7);
} else if (cleaned.length() == 10) {
return cleaned.substring(0, 3) + "-***-" + cleaned.substring(6);
}
return phoneNumber;
}
/**
* 사업자번호 마스킹 처리
* 예: 123-45-67890 → 123-**-****0
*
* @param businessNumber 사업자번호
* @return 마스킹된 사업자번호
*/
public static String maskBusinessNumber(String businessNumber) {
if (isBlank(businessNumber)) {
return businessNumber;
}
String cleaned = businessNumber.replaceAll("[^0-9]", "");
if (cleaned.length() == 10) {
return cleaned.substring(0, 3) + "-**-****" + cleaned.substring(9);
}
return businessNumber;
}
/**
* 이메일 마스킹 처리
* 예: user@example.com → u***@example.com
*
* @param email 이메일
* @return 마스킹된 이메일
*/
public static String maskEmail(String email) {
if (isBlank(email) || !email.contains("@")) {
return email;
}
String[] parts = email.split("@");
String localPart = parts[0];
String domain = parts[1];
if (localPart.length() <= 1) {
return email;
}
String masked = localPart.charAt(0) + "***";
return masked + "@" + domain;
}
/**
* 문자열을 지정된 길이로 자르고 말줄임표 추가
*
* @param str 원본 문자열
* @param maxLength 최대 길이
* @return 잘린 문자열
*/
public static String truncate(String str, int maxLength) {
if (isBlank(str) || str.length() <= maxLength) {
return str;
}
return str.substring(0, maxLength) + "...";
}
/**
* null인 경우 기본값 반환
*
* @param str 원본 문자열
* @param defaultValue 기본값
* @return 원본 또는 기본값
*/
public static String defaultIfBlank(String str, String defaultValue) {
return StringUtils.defaultIfBlank(str, defaultValue);
}
/**
* 문자열에서 공백 제거
*
* @param str 원본 문자열
* @return 공백이 제거된 문자열
*/
public static String removeWhitespace(String str) {
if (isBlank(str)) {
return str;
}
return str.replaceAll("\\s+", "");
}
/**
* 전화번호 포맷 검증
*
* @param phoneNumber 전화번호
* @return 유효한 전화번호 형식 여부
*/
public static boolean isValidPhoneNumber(String phoneNumber) {
if (isBlank(phoneNumber)) {
return false;
}
String cleaned = phoneNumber.replaceAll("[^0-9]", "");
return cleaned.length() >= 10 && cleaned.length() <= 11;
}
/**
* 이메일 포맷 검증
*
* @param email 이메일
* @return 유효한 이메일 형식 여부
*/
public static boolean isValidEmail(String email) {
if (isBlank(email)) {
return false;
}
String emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}$";
return email.matches(emailRegex);
}
/**
* 사업자번호 포맷 검증
*
* @param businessNumber 사업자번호
* @return 유효한 사업자번호 형식 여부
*/
public static boolean isValidBusinessNumber(String businessNumber) {
if (isBlank(businessNumber)) {
return false;
}
String cleaned = businessNumber.replaceAll("[^0-9]", "");
return cleaned.length() == 10;
}
}
@@ -0,0 +1,173 @@
package com.kt.event.common.util;
import com.kt.event.common.exception.BusinessException;
import com.kt.event.common.exception.ErrorCode;
/**
* 유효성 검증 유틸리티
* 비즈니스 로직에서 사용하는 공통 유효성 검증 기능 제공
*/
public class ValidationUtil {
/**
* null 체크 및 예외 발생
*
* @param object 검증할 객체
* @param errorCode 에러 코드
* @throws BusinessException 객체가 null인 경우
*/
public static void requireNonNull(Object object, ErrorCode errorCode) {
if (object == null) {
throw new BusinessException(errorCode);
}
}
/**
* null 체크 및 예외 발생 (커스텀 메시지)
*
* @param object 검증할 객체
* @param errorCode 에러 코드
* @param message 커스텀 메시지
* @throws BusinessException 객체가 null인 경우
*/
public static void requireNonNull(Object object, ErrorCode errorCode, String message) {
if (object == null) {
throw new BusinessException(errorCode, message);
}
}
/**
* 문자열 공백 체크 및 예외 발생
*
* @param str 검증할 문자열
* @param errorCode 에러 코드
* @throws BusinessException 문자열이 null이거나 공백인 경우
*/
public static void requireNotBlank(String str, ErrorCode errorCode) {
if (StringUtil.isBlank(str)) {
throw new BusinessException(errorCode);
}
}
/**
* 문자열 공백 체크 및 예외 발생 (커스텀 메시지)
*
* @param str 검증할 문자열
* @param errorCode 에러 코드
* @param message 커스텀 메시지
* @throws BusinessException 문자열이 null이거나 공백인 경우
*/
public static void requireNotBlank(String str, ErrorCode errorCode, String message) {
if (StringUtil.isBlank(str)) {
throw new BusinessException(errorCode, message);
}
}
/**
* 조건 검증 및 예외 발생
*
* @param condition 검증할 조건
* @param errorCode 에러 코드
* @throws BusinessException 조건이 false인 경우
*/
public static void require(boolean condition, ErrorCode errorCode) {
if (!condition) {
throw new BusinessException(errorCode);
}
}
/**
* 조건 검증 및 예외 발생 (커스텀 메시지)
*
* @param condition 검증할 조건
* @param errorCode 에러 코드
* @param message 커스텀 메시지
* @throws BusinessException 조건이 false인 경우
*/
public static void require(boolean condition, ErrorCode errorCode, String message) {
if (!condition) {
throw new BusinessException(errorCode, message);
}
}
/**
* 전화번호 유효성 검증
*
* @param phoneNumber 전화번호
* @param errorCode 에러 코드
* @throws BusinessException 유효하지 않은 전화번호인 경우
*/
public static void requireValidPhoneNumber(String phoneNumber, ErrorCode errorCode) {
if (!StringUtil.isValidPhoneNumber(phoneNumber)) {
throw new BusinessException(errorCode, "유효하지 않은 전화번호입니다: " + phoneNumber);
}
}
/**
* 이메일 유효성 검증
*
* @param email 이메일
* @param errorCode 에러 코드
* @throws BusinessException 유효하지 않은 이메일인 경우
*/
public static void requireValidEmail(String email, ErrorCode errorCode) {
if (!StringUtil.isValidEmail(email)) {
throw new BusinessException(errorCode, "유효하지 않은 이메일입니다: " + email);
}
}
/**
* 사업자번호 유효성 검증
*
* @param businessNumber 사업자번호
* @param errorCode 에러 코드
* @throws BusinessException 유효하지 않은 사업자번호인 경우
*/
public static void requireValidBusinessNumber(String businessNumber, ErrorCode errorCode) {
if (!StringUtil.isValidBusinessNumber(businessNumber)) {
throw new BusinessException(errorCode, "유효하지 않은 사업자번호입니다: " + businessNumber);
}
}
/**
* 양수 검증
*
* @param value 검증할 값
* @param errorCode 에러 코드
* @throws BusinessException 값이 0보다 작거나 같은 경우
*/
public static void requirePositive(long value, ErrorCode errorCode) {
if (value <= 0) {
throw new BusinessException(errorCode, "값은 양수여야 합니다: " + value);
}
}
/**
* 음수 아닌 값 검증
*
* @param value 검증할 값
* @param errorCode 에러 코드
* @throws BusinessException 값이 0보다 작은 경우
*/
public static void requireNonNegative(long value, ErrorCode errorCode) {
if (value < 0) {
throw new BusinessException(errorCode, "값은 음수가 아니어야 합니다: " + value);
}
}
/**
* 범위 검증
*
* @param value 검증할 값
* @param min 최소값
* @param max 최대값
* @param errorCode 에러 코드
* @throws BusinessException 값이 범위를 벗어난 경우
*/
public static void requireInRange(long value, long min, long max, ErrorCode errorCode) {
if (value < min || value > max) {
throw new BusinessException(errorCode,
String.format("값은 %d ~ %d 범위여야 합니다: %d", min, max, value));
}
}
}