mirror of
https://github.com/ktds-dg0501/kt-event-marketing.git
synced 2026-06-12 23:19:10 +00:00
add common module
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user