mirror of
https://github.com/cna-bootcamp/phonebill.git
synced 2026-06-12 19:49:10 +00:00
release
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
// Common 모듈: 일반 jar 생성
|
||||
// java-library 플러그인 추가로 api/implementation 사용 가능
|
||||
apply plugin: 'java-library'
|
||||
|
||||
// Spring Boot BOM 추가 (의존성 관리를 위해)
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom "org.springframework.boot:spring-boot-dependencies:3.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
enabled = true
|
||||
archiveClassifier = ''
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Spring Boot Starters
|
||||
api 'org.springframework.boot:spring-boot-starter-web'
|
||||
api 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
api 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||
api 'org.springframework.boot:spring-boot-starter-security'
|
||||
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}"
|
||||
|
||||
// MapStruct
|
||||
api "org.mapstruct:mapstruct:${mapstructVersion}"
|
||||
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
|
||||
|
||||
// Jackson
|
||||
api 'com.fasterxml.jackson.core:jackson-databind'
|
||||
api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.phonebill.common.aop;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 로깅 AOP
|
||||
* 메소드 실행 시간과 파라미터를 로깅합니다.
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
public class LoggingAspect {
|
||||
|
||||
@Around("execution(* com.phonebill..service..*(..))")
|
||||
public Object logServiceMethods(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
String className = joinPoint.getTarget().getClass().getSimpleName();
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
Object[] args = joinPoint.getArgs();
|
||||
|
||||
log.info("[SERVICE] {}.{}() called with args: {}", className, methodName, args);
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
try {
|
||||
Object result = joinPoint.proceed();
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
log.info("[SERVICE] {}.{}() completed in {}ms", className, methodName, executionTime);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
log.error("[SERVICE] {}.{}() failed in {}ms with error: {}", className, methodName, executionTime, e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Around("execution(* com.phonebill..controller..*(..))")
|
||||
public Object logControllerMethods(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
String className = joinPoint.getTarget().getClass().getSimpleName();
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
Object[] args = joinPoint.getArgs();
|
||||
|
||||
log.info("[CONTROLLER] {}.{}() called with args: {}", className, methodName, args);
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
try {
|
||||
Object result = joinPoint.proceed();
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
log.info("[CONTROLLER] {}.{}() completed in {}ms", className, methodName, executionTime);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
log.error("[CONTROLLER] {}.{}() failed in {}ms with error: {}", className, methodName, executionTime, e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.phonebill.common.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
|
||||
/**
|
||||
* JPA 설정
|
||||
* JPA Auditing과 Repository 설정을 제공합니다.
|
||||
*/
|
||||
@Configuration
|
||||
@EnableJpaAuditing
|
||||
@EnableJpaRepositories(basePackages = "com.phonebill")
|
||||
public class JpaConfig {
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.phonebill.common.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 표준 API 응답 DTO
|
||||
* 모든 API 응답의 일관성을 보장하기 위한 공통 응답 구조
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class ApiResponse<T> {
|
||||
|
||||
/**
|
||||
* 응답 성공 여부
|
||||
*/
|
||||
private boolean success;
|
||||
|
||||
/**
|
||||
* 응답 메시지
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 응답 데이터
|
||||
*/
|
||||
private T data;
|
||||
|
||||
/**
|
||||
* 오류 코드 (실패시)
|
||||
*/
|
||||
private String errorCode;
|
||||
|
||||
/**
|
||||
* 타임스탬프
|
||||
*/
|
||||
private long timestamp;
|
||||
|
||||
private ApiResponse(boolean success, String message, T data, String errorCode) {
|
||||
this.success = success;
|
||||
this.message = message;
|
||||
this.data = data;
|
||||
this.errorCode = errorCode;
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* 성공 응답 생성
|
||||
*/
|
||||
public static <T> ApiResponse<T> success(T data) {
|
||||
return new ApiResponse<>(true, "Success", data, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 성공 응답 생성 (메시지 포함)
|
||||
*/
|
||||
public static <T> ApiResponse<T> success(String message, T data) {
|
||||
return new ApiResponse<>(true, message, data, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 실패 응답 생성
|
||||
*/
|
||||
public static <T> ApiResponse<T> error(String message) {
|
||||
return new ApiResponse<>(false, message, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 실패 응답 생성 (오류 코드 포함)
|
||||
*/
|
||||
public static <T> ApiResponse<T> error(String message, String errorCode) {
|
||||
return new ApiResponse<>(false, message, null, errorCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.phonebill.common.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 오류 응답 구조
|
||||
* API 오류 발생 시 표준화된 응답 형식을 제공합니다.
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ErrorResponse {
|
||||
|
||||
private String code;
|
||||
private String message;
|
||||
private String detail;
|
||||
private LocalDateTime timestamp;
|
||||
private String path;
|
||||
|
||||
public static ErrorResponse of(String code, String message) {
|
||||
return ErrorResponse.builder()
|
||||
.code(code)
|
||||
.message(message)
|
||||
.timestamp(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static ErrorResponse of(String code, String message, String detail) {
|
||||
return ErrorResponse.builder()
|
||||
.code(code)
|
||||
.message(message)
|
||||
.detail(detail)
|
||||
.timestamp(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static ErrorResponse of(String code, String message, String detail, String path) {
|
||||
return ErrorResponse.builder()
|
||||
.code(code)
|
||||
.message(message)
|
||||
.detail(detail)
|
||||
.timestamp(LocalDateTime.now())
|
||||
.path(path)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.phonebill.common.dto;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 페이징 요청 DTO
|
||||
* 목록 조회시 페이징 처리를 위한 공통 요청 구조
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class PageableRequest {
|
||||
|
||||
/**
|
||||
* 페이지 번호 (0부터 시작)
|
||||
*/
|
||||
@Min(value = 0, message = "페이지 번호는 0 이상이어야 합니다.")
|
||||
private int page = 0;
|
||||
|
||||
/**
|
||||
* 페이지 크기
|
||||
*/
|
||||
@Min(value = 1, message = "페이지 크기는 1 이상이어야 합니다.")
|
||||
private int size = 20;
|
||||
|
||||
/**
|
||||
* 정렬 기준 (예: "id,desc" 또는 "name,asc")
|
||||
*/
|
||||
private String sort;
|
||||
|
||||
public PageableRequest(int page, int size) {
|
||||
this.page = page;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public PageableRequest(int page, int size, String sort) {
|
||||
this.page = page;
|
||||
this.size = size;
|
||||
this.sort = sort;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.phonebill.common.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 페이징 응답 DTO
|
||||
* 목록 조회 결과의 페이징 정보를 포함하는 공통 응답 구조
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class PageableResponse<T> {
|
||||
|
||||
/**
|
||||
* 실제 데이터 목록
|
||||
*/
|
||||
private List<T> content;
|
||||
|
||||
/**
|
||||
* 현재 페이지 번호 (0부터 시작)
|
||||
*/
|
||||
private int page;
|
||||
|
||||
/**
|
||||
* 페이지 크기
|
||||
*/
|
||||
private int size;
|
||||
|
||||
/**
|
||||
* 전체 요소 개수
|
||||
*/
|
||||
private long totalElements;
|
||||
|
||||
/**
|
||||
* 전체 페이지 수
|
||||
*/
|
||||
private int totalPages;
|
||||
|
||||
/**
|
||||
* 첫 번째 페이지 여부
|
||||
*/
|
||||
private boolean first;
|
||||
|
||||
/**
|
||||
* 마지막 페이지 여부
|
||||
*/
|
||||
private boolean last;
|
||||
|
||||
/**
|
||||
* 정렬 기준
|
||||
*/
|
||||
private String sort;
|
||||
|
||||
public PageableResponse(List<T> content, int page, int size, long totalElements, String sort) {
|
||||
this.content = content;
|
||||
this.page = page;
|
||||
this.size = size;
|
||||
this.totalElements = totalElements;
|
||||
this.totalPages = (int) Math.ceil((double) totalElements / size);
|
||||
this.first = page == 0;
|
||||
this.last = page >= totalPages - 1;
|
||||
this.sort = sort;
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이징 응답 생성
|
||||
*/
|
||||
public static <T> PageableResponse<T> of(List<T> content, PageableRequest request, long totalElements) {
|
||||
return new PageableResponse<>(content, request.getPage(), request.getSize(), totalElements, request.getSort());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.phonebill.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;
|
||||
|
||||
/**
|
||||
* 기본 엔티티 클래스
|
||||
* 생성일시, 수정일시를 자동으로 관리하는 JPA Auditing 기능을 제공합니다.
|
||||
*/
|
||||
@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,51 @@
|
||||
package com.phonebill.common.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 비즈니스 로직 처리 중 발생하는 예외
|
||||
* 일반적인 업무 처리 과정에서 예상되는 오류 상황을 나타냄
|
||||
*/
|
||||
@Getter
|
||||
public class BusinessException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* 오류 코드
|
||||
*/
|
||||
private final String errorCode;
|
||||
|
||||
/**
|
||||
* HTTP 상태 코드
|
||||
*/
|
||||
private final int httpStatus;
|
||||
|
||||
public BusinessException(String message) {
|
||||
super(message);
|
||||
this.errorCode = "BUSINESS_ERROR";
|
||||
this.httpStatus = 400; // Bad Request
|
||||
}
|
||||
|
||||
public BusinessException(String message, String errorCode) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
this.httpStatus = 400; // Bad Request
|
||||
}
|
||||
|
||||
public BusinessException(String message, String errorCode, int httpStatus) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
this.httpStatus = httpStatus;
|
||||
}
|
||||
|
||||
public BusinessException(String message, String errorCode, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.errorCode = errorCode;
|
||||
this.httpStatus = 400; // Bad Request
|
||||
}
|
||||
|
||||
public BusinessException(String message, String errorCode, int httpStatus, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.errorCode = errorCode;
|
||||
this.httpStatus = httpStatus;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.phonebill.common.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 오류 코드 열거형
|
||||
* 시스템 전체에서 사용되는 표준화된 오류 코드를 정의합니다.
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum ErrorCode {
|
||||
|
||||
// 공통 오류
|
||||
INTERNAL_SERVER_ERROR("E0001", "내부 서버 오류가 발생했습니다."),
|
||||
INVALID_INPUT_VALUE("E0002", "입력값이 올바르지 않습니다."),
|
||||
METHOD_NOT_ALLOWED("E0003", "허용되지 않은 HTTP 메소드입니다."),
|
||||
ENTITY_NOT_FOUND("E0004", "요청한 리소스를 찾을 수 없습니다."),
|
||||
INVALID_TYPE_VALUE("E0005", "잘못된 타입의 값입니다."),
|
||||
HANDLE_ACCESS_DENIED("E0006", "접근이 거부되었습니다."),
|
||||
|
||||
// 인증/인가 오류
|
||||
UNAUTHORIZED("E1001", "인증이 필요합니다."),
|
||||
FORBIDDEN("E1002", "권한이 없습니다."),
|
||||
INVALID_TOKEN("E1003", "유효하지 않은 토큰입니다."),
|
||||
TOKEN_EXPIRED("E1004", "토큰이 만료되었습니다."),
|
||||
LOGIN_REQUIRED("E1005", "로그인이 필요합니다."),
|
||||
ACCOUNT_LOCKED("E1006", "계정이 잠겨있습니다."),
|
||||
INVALID_CREDENTIALS("E1007", "잘못된 인증 정보입니다."),
|
||||
|
||||
// 비즈니스 오류
|
||||
BUSINESS_ERROR("E2001", "비즈니스 로직 오류가 발생했습니다."),
|
||||
VALIDATION_ERROR("E2002", "검증 오류가 발생했습니다."),
|
||||
DUPLICATE_RESOURCE("E2003", "중복된 리소스입니다."),
|
||||
RESOURCE_NOT_FOUND("E2004", "요청한 리소스를 찾을 수 없습니다."),
|
||||
OPERATION_NOT_ALLOWED("E2005", "허용되지 않은 작업입니다."),
|
||||
|
||||
// 외부 시스템 연동 오류
|
||||
EXTERNAL_SYSTEM_ERROR("E3001", "외부 시스템 연동 오류가 발생했습니다."),
|
||||
CIRCUIT_BREAKER_OPEN("E3002", "외부 시스템이 일시적으로 사용할 수 없습니다."),
|
||||
TIMEOUT_ERROR("E3003", "요청 시간이 초과되었습니다."),
|
||||
CONNECTION_ERROR("E3004", "연결 오류가 발생했습니다."),
|
||||
|
||||
// 데이터베이스 오류
|
||||
DATABASE_ERROR("E4001", "데이터베이스 오류가 발생했습니다."),
|
||||
CONSTRAINT_VIOLATION("E4002", "데이터 제약 조건 위반이 발생했습니다."),
|
||||
TRANSACTION_ERROR("E4003", "트랜잭션 오류가 발생했습니다."),
|
||||
|
||||
// 캐시 오류
|
||||
CACHE_ERROR("E5001", "캐시 오류가 발생했습니다."),
|
||||
CACHE_NOT_FOUND("E5002", "캐시에서 데이터를 찾을 수 없습니다."),
|
||||
|
||||
// 요금조회 관련 오류
|
||||
BILL_INQUIRY_ERROR("E6001", "요금조회 중 오류가 발생했습니다."),
|
||||
BILL_NOT_FOUND("E6002", "요금 정보를 찾을 수 없습니다."),
|
||||
BILL_INQUIRY_FAILED("E6003", "요금조회에 실패했습니다."),
|
||||
|
||||
// 상품변경 관련 오류
|
||||
PRODUCT_CHANGE_ERROR("E7001", "상품변경 중 오류가 발생했습니다."),
|
||||
PRODUCT_NOT_FOUND("E7002", "상품 정보를 찾을 수 없습니다."),
|
||||
PRODUCT_VALIDATION_ERROR("E7003", "상품변경 검증에 실패했습니다."),
|
||||
PRODUCT_CHANGE_FAILED("E7004", "상품변경에 실패했습니다."),
|
||||
|
||||
// KOS 연동 오류
|
||||
KOS_CONNECTION_ERROR("E8001", "KOS 시스템 연결 오류가 발생했습니다."),
|
||||
KOS_RESPONSE_ERROR("E8002", "KOS 시스템 응답 오류가 발생했습니다."),
|
||||
KOS_TIMEOUT_ERROR("E8003", "KOS 시스템 응답 시간 초과가 발생했습니다."),
|
||||
KOS_SERVICE_UNAVAILABLE("E8004", "KOS 시스템이 일시적으로 사용할 수 없습니다.");
|
||||
|
||||
private final String code;
|
||||
private final String message;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.phonebill.common.exception;
|
||||
|
||||
import com.phonebill.common.dto.ApiResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 전역 예외 처리기
|
||||
* 모든 컨트롤러에서 발생하는 예외를 일관된 형태로 처리
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/**
|
||||
* 비즈니스 예외 처리
|
||||
*/
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException ex) {
|
||||
log.warn("Business exception occurred: {}", ex.getMessage());
|
||||
|
||||
ApiResponse<Void> response = ApiResponse.error(ex.getMessage(), ex.getErrorCode());
|
||||
return ResponseEntity.status(ex.getHttpStatus()).body(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 유효성 검증 실패 예외 처리
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationException(MethodArgumentNotValidException ex) {
|
||||
log.warn("Validation exception occurred: {}", ex.getMessage());
|
||||
|
||||
Map<String, String> errors = new HashMap<>();
|
||||
ex.getBindingResult().getFieldErrors().forEach(error -> {
|
||||
errors.put(error.getField(), error.getDefaultMessage());
|
||||
});
|
||||
|
||||
ApiResponse<Map<String, String>> response = ApiResponse.error("입력값이 올바르지 않습니다.", "VALIDATION_ERROR");
|
||||
response.setData(errors);
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 일반적인 예외 처리
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleException(Exception ex) {
|
||||
log.error("Unexpected exception occurred", ex);
|
||||
|
||||
ApiResponse<Void> response = ApiResponse.error("서버 내부 오류가 발생했습니다.", "INTERNAL_SERVER_ERROR");
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* IllegalArgumentException 처리
|
||||
*/
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleIllegalArgumentException(IllegalArgumentException ex) {
|
||||
log.warn("Illegal argument exception occurred: {}", ex.getMessage());
|
||||
|
||||
ApiResponse<Void> response = ApiResponse.error(ex.getMessage(), "INVALID_ARGUMENT");
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* RuntimeException 처리
|
||||
*/
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleRuntimeException(RuntimeException ex) {
|
||||
log.error("Runtime exception occurred", ex);
|
||||
|
||||
ApiResponse<Void> response = ApiResponse.error("처리 중 오류가 발생했습니다.", "RUNTIME_ERROR");
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.phonebill.common.exception;
|
||||
|
||||
/**
|
||||
* 인프라 예외
|
||||
* 데이터베이스, 캐시, 외부 시스템 연동 등 인프라 관련 오류를 나타냅니다.
|
||||
*/
|
||||
public class InfraException extends RuntimeException {
|
||||
|
||||
private final ErrorCode errorCode;
|
||||
|
||||
public InfraException(ErrorCode errorCode) {
|
||||
super(errorCode.getMessage());
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public InfraException(ErrorCode errorCode, String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public InfraException(ErrorCode errorCode, Throwable cause) {
|
||||
super(errorCode.getMessage(), cause);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public InfraException(ErrorCode errorCode, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public ErrorCode getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.phonebill.common.exception;
|
||||
|
||||
/**
|
||||
* 리소스를 찾을 수 없는 경우 발생하는 예외
|
||||
* 사용자, 요금제, 청구서 등의 데이터가 존재하지 않을 때 사용
|
||||
*/
|
||||
public class ResourceNotFoundException extends BusinessException {
|
||||
|
||||
public ResourceNotFoundException(String message) {
|
||||
super(message, "RESOURCE_NOT_FOUND", 404);
|
||||
}
|
||||
|
||||
public ResourceNotFoundException(String resourceType, Object id) {
|
||||
super(String.format("%s를 찾을 수 없습니다. ID: %s", resourceType, id), "RESOURCE_NOT_FOUND", 404);
|
||||
}
|
||||
|
||||
public ResourceNotFoundException(String message, Throwable cause) {
|
||||
super(message, "RESOURCE_NOT_FOUND", 404, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.phonebill.common.exception;
|
||||
|
||||
/**
|
||||
* 인증되지 않은 요청에 대한 예외
|
||||
* JWT 토큰이 유효하지 않거나 만료된 경우 발생
|
||||
*/
|
||||
public class UnauthorizedException extends BusinessException {
|
||||
|
||||
public UnauthorizedException(String message) {
|
||||
super(message, "UNAUTHORIZED", 401);
|
||||
}
|
||||
|
||||
public UnauthorizedException() {
|
||||
super("인증이 필요합니다.", "UNAUTHORIZED", 401);
|
||||
}
|
||||
|
||||
public UnauthorizedException(String message, Throwable cause) {
|
||||
super(message, "UNAUTHORIZED", 401, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.phonebill.common.exception;
|
||||
|
||||
/**
|
||||
* 데이터 검증 실패시 발생하는 예외
|
||||
* 입력 데이터가 비즈니스 규칙에 맞지 않을 때 사용
|
||||
*/
|
||||
public class ValidationException extends BusinessException {
|
||||
|
||||
public ValidationException(String message) {
|
||||
super(message, "VALIDATION_ERROR", 400);
|
||||
}
|
||||
|
||||
public ValidationException(String field, String message) {
|
||||
super(String.format("%s: %s", field, message), "VALIDATION_ERROR", 400);
|
||||
}
|
||||
|
||||
public ValidationException(String message, Throwable cause) {
|
||||
super(message, "VALIDATION_ERROR", 400, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.phonebill.common.security;
|
||||
|
||||
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.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* JWT 인증 필터
|
||||
* HTTP 요청에서 JWT 토큰을 추출하여 인증을 수행
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
|
||||
String token = jwtTokenProvider.resolveToken(request);
|
||||
|
||||
if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
|
||||
String userId = jwtTokenProvider.getUserId(token);
|
||||
String username = null;
|
||||
String authority = null;
|
||||
|
||||
try {
|
||||
username = jwtTokenProvider.getUsername(token);
|
||||
} catch (Exception e) {
|
||||
log.debug("JWT에 username 클레임이 없음: {}", e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
authority = jwtTokenProvider.getAuthority(token);
|
||||
} catch (Exception e) {
|
||||
log.debug("JWT에 authority 클레임이 없음: {}", e.getMessage());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(userId)) {
|
||||
// UserPrincipal 객체 생성 (username과 authority가 없어도 동작)
|
||||
UserPrincipal userPrincipal = UserPrincipal.builder()
|
||||
.userId(userId)
|
||||
.username(username != null ? username : "unknown")
|
||||
.authority(authority != null ? authority : "USER")
|
||||
.build();
|
||||
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(
|
||||
userPrincipal,
|
||||
null,
|
||||
Collections.singletonList(new SimpleGrantedAuthority(authority != null ? authority : "USER"))
|
||||
);
|
||||
|
||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
log.debug("인증된 사용자: {} ({})", userPrincipal.getUsername(), userId);
|
||||
}
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
String path = request.getRequestURI();
|
||||
return path.startsWith("/actuator") ||
|
||||
path.startsWith("/swagger-ui") ||
|
||||
path.startsWith("/v3/api-docs") ||
|
||||
path.equals("/health");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.phonebill.common.security;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.ExpiredJwtException;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.MalformedJwtException;
|
||||
import io.jsonwebtoken.UnsupportedJwtException;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import io.jsonwebtoken.security.SecurityException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* JWT 토큰 제공자
|
||||
* JWT 토큰의 생성, 검증, 파싱을 담당
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JwtTokenProvider {
|
||||
|
||||
private final SecretKey secretKey;
|
||||
private final long tokenValidityInMilliseconds;
|
||||
|
||||
public JwtTokenProvider(@Value("${security.jwt.secret:}") String secret,
|
||||
@Value("${security.jwt.access-token-expiration:3600}") long tokenValidityInSeconds) {
|
||||
if (StringUtils.hasText(secret)) {
|
||||
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
||||
} else {
|
||||
// 개발용 기본 시크릿 키 (32바이트 이상)
|
||||
this.secretKey = Keys.hmacShaKeyFor("phonebill-default-secret-key-for-development-only".getBytes(StandardCharsets.UTF_8));
|
||||
log.warn("JWT secret key not provided, using default development key");
|
||||
}
|
||||
this.tokenValidityInMilliseconds = tokenValidityInSeconds * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 요청에서 JWT 토큰 추출
|
||||
*/
|
||||
public String resolveToken(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader("Authorization");
|
||||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
|
||||
return bearerToken.substring(7);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT 토큰 유효성 검증
|
||||
*/
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
Jwts.parser()
|
||||
.verifyWith(secretKey)
|
||||
.build()
|
||||
.parseSignedClaims(token);
|
||||
return true;
|
||||
} catch (SecurityException | MalformedJwtException e) {
|
||||
log.debug("Invalid JWT signature: {}", e.getMessage());
|
||||
} catch (ExpiredJwtException e) {
|
||||
log.debug("Expired JWT token: {}", e.getMessage());
|
||||
} catch (UnsupportedJwtException e) {
|
||||
log.debug("Unsupported JWT token: {}", e.getMessage());
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.debug("JWT token compact of handler are invalid: {}", e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT 토큰에서 사용자 ID 추출
|
||||
*/
|
||||
public String getUserId(String token) {
|
||||
Claims claims = Jwts.parser()
|
||||
.verifyWith(secretKey)
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
|
||||
return claims.getSubject();
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT 토큰에서 사용자명 추출
|
||||
*/
|
||||
public String getUsername(String token) {
|
||||
Claims claims = Jwts.parser()
|
||||
.verifyWith(secretKey)
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
|
||||
return claims.get("username", String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT 토큰에서 권한 정보 추출
|
||||
*/
|
||||
public String getAuthority(String token) {
|
||||
Claims claims = Jwts.parser()
|
||||
.verifyWith(secretKey)
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
|
||||
return claims.get("authority", String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰 만료 시간 확인
|
||||
*/
|
||||
public boolean isTokenExpired(String token) {
|
||||
try {
|
||||
Claims claims = Jwts.parser()
|
||||
.verifyWith(secretKey)
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
|
||||
return claims.getExpiration().before(new Date());
|
||||
} catch (Exception e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰에서 만료 시간 추출
|
||||
*/
|
||||
public Date getExpirationDate(String token) {
|
||||
Claims claims = Jwts.parser()
|
||||
.verifyWith(secretKey)
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
|
||||
return claims.getExpiration();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.phonebill.common.security;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 인증된 사용자 정보
|
||||
* JWT 토큰에서 추출된 사용자 정보를 담는 Principal 객체
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@RequiredArgsConstructor
|
||||
public class UserPrincipal {
|
||||
|
||||
/**
|
||||
* 사용자 고유 ID
|
||||
*/
|
||||
private final String userId;
|
||||
|
||||
/**
|
||||
* 사용자명
|
||||
*/
|
||||
private final String username;
|
||||
|
||||
/**
|
||||
* 사용자 권한
|
||||
*/
|
||||
private final String authority;
|
||||
|
||||
/**
|
||||
* 사용자 ID 반환 (별칭)
|
||||
*/
|
||||
public String getName() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 권한 여부 확인
|
||||
*/
|
||||
public boolean isAdmin() {
|
||||
return "ADMIN".equals(authority);
|
||||
}
|
||||
|
||||
/**
|
||||
* 일반 사용자 권한 여부 확인
|
||||
*/
|
||||
public boolean isUser() {
|
||||
return "USER".equals(authority) || authority == null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.phonebill.common.util;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* 날짜/시간 관련 유틸리티
|
||||
* 날짜 포맷팅, 파싱 등의 공통 기능을 제공
|
||||
*/
|
||||
public class DateTimeUtils {
|
||||
|
||||
/**
|
||||
* 표준 날짜/시간 포맷터
|
||||
*/
|
||||
public static final DateTimeFormatter STANDARD_DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
/**
|
||||
* 날짜 포맷터
|
||||
*/
|
||||
public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
/**
|
||||
* 시간 포맷터
|
||||
*/
|
||||
public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
|
||||
|
||||
/**
|
||||
* ISO 8601 포맷터
|
||||
*/
|
||||
public static final DateTimeFormatter ISO_DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
|
||||
|
||||
/**
|
||||
* LocalDateTime을 문자열로 변환
|
||||
*/
|
||||
public static String format(LocalDateTime dateTime) {
|
||||
if (dateTime == null) {
|
||||
return null;
|
||||
}
|
||||
return dateTime.format(STANDARD_DATETIME_FORMATTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* LocalDateTime을 지정된 포맷으로 변환
|
||||
*/
|
||||
public static String format(LocalDateTime dateTime, DateTimeFormatter formatter) {
|
||||
if (dateTime == null) {
|
||||
return null;
|
||||
}
|
||||
return dateTime.format(formatter);
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열을 LocalDateTime으로 파싱
|
||||
*/
|
||||
public static LocalDateTime parse(String dateTimeString) {
|
||||
if (dateTimeString == null || dateTimeString.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return LocalDateTime.parse(dateTimeString, STANDARD_DATETIME_FORMATTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열을 지정된 포맷으로 LocalDateTime으로 파싱
|
||||
*/
|
||||
public static LocalDateTime parse(String dateTimeString, DateTimeFormatter formatter) {
|
||||
if (dateTimeString == null || dateTimeString.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return LocalDateTime.parse(dateTimeString, formatter);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 날짜/시간을 표준 포맷으로 반환
|
||||
*/
|
||||
public static String getCurrentDateTime() {
|
||||
return LocalDateTime.now().format(STANDARD_DATETIME_FORMATTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 날짜를 반환
|
||||
*/
|
||||
public static String getCurrentDate() {
|
||||
return LocalDateTime.now().format(DATE_FORMATTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 시간을 반환
|
||||
*/
|
||||
public static String getCurrentTime() {
|
||||
return LocalDateTime.now().format(TIME_FORMATTER);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.phonebill.common.util;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
/**
|
||||
* 날짜 유틸리티
|
||||
* 날짜 관련 공통 기능을 제공합니다.
|
||||
*/
|
||||
public class DateUtil {
|
||||
|
||||
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
|
||||
public static final String DEFAULT_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
|
||||
public static final String DEFAULT_TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS";
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT);
|
||||
private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_DATETIME_FORMAT);
|
||||
private static final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_TIMESTAMP_FORMAT);
|
||||
|
||||
/**
|
||||
* 현재 날짜를 문자열로 반환
|
||||
*/
|
||||
public static String getCurrentDateString() {
|
||||
return LocalDate.now().format(DATE_FORMATTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 날짜시간을 문자열로 반환
|
||||
*/
|
||||
public static String getCurrentDateTimeString() {
|
||||
return LocalDateTime.now().format(DATETIME_FORMATTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 타임스탬프를 문자열로 반환
|
||||
*/
|
||||
public static String getCurrentTimestampString() {
|
||||
return LocalDateTime.now().format(TIMESTAMP_FORMATTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* LocalDate를 문자열로 변환
|
||||
*/
|
||||
public static String formatDate(LocalDate date) {
|
||||
return date != null ? date.format(DATE_FORMATTER) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* LocalDateTime을 문자열로 변환
|
||||
*/
|
||||
public static String formatDateTime(LocalDateTime dateTime) {
|
||||
return dateTime != null ? dateTime.format(DATETIME_FORMATTER) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열을 LocalDate로 변환
|
||||
*/
|
||||
public static LocalDate parseDate(String dateString) {
|
||||
if (dateString == null || dateString.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(dateString, DATE_FORMATTER);
|
||||
} catch (DateTimeParseException e) {
|
||||
throw new IllegalArgumentException("Invalid date format: " + dateString, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열을 LocalDateTime으로 변환
|
||||
*/
|
||||
public static LocalDateTime parseDateTime(String dateTimeString) {
|
||||
if (dateTimeString == null || dateTimeString.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDateTime.parse(dateTimeString, DATETIME_FORMATTER);
|
||||
} catch (DateTimeParseException e) {
|
||||
throw new IllegalArgumentException("Invalid datetime format: " + dateTimeString, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜 유효성 검사
|
||||
*/
|
||||
public static boolean isValidDate(String dateString) {
|
||||
try {
|
||||
parseDate(dateString);
|
||||
return true;
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜시간 유효성 검사
|
||||
*/
|
||||
public static boolean isValidDateTime(String dateTimeString) {
|
||||
try {
|
||||
parseDateTime(dateTimeString);
|
||||
return true;
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.phonebill.common.util;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 보안 유틸리티
|
||||
* Spring Security 관련 공통 기능을 제공합니다.
|
||||
*/
|
||||
public class SecurityUtil {
|
||||
|
||||
/**
|
||||
* 현재 인증된 사용자 ID를 반환
|
||||
*/
|
||||
public static Optional<String> getCurrentUserId() {
|
||||
return getCurrentUserDetails()
|
||||
.map(UserDetails::getUsername);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 인증된 사용자 정보를 반환
|
||||
*/
|
||||
public static Optional<UserDetails> getCurrentUserDetails() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Object principal = authentication.getPrincipal();
|
||||
if (principal instanceof UserDetails) {
|
||||
return Optional.of((UserDetails) principal);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 인증된 사용자의 권한을 확인
|
||||
*/
|
||||
public static boolean hasAuthority(String authority) {
|
||||
return getCurrentUserDetails()
|
||||
.map(user -> user.getAuthorities().stream()
|
||||
.anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals(authority)))
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 인증된 사용자가 특정 역할을 가지고 있는지 확인
|
||||
*/
|
||||
public static boolean hasRole(String role) {
|
||||
return hasAuthority("ROLE_" + role);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 인증된 사용자가 인증되었는지 확인
|
||||
*/
|
||||
public static boolean isAuthenticated() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
return authentication != null && authentication.isAuthenticated()
|
||||
&& !"anonymousUser".equals(authentication.getPrincipal());
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 인증된 사용자의 인증 정보를 반환
|
||||
*/
|
||||
public static Optional<Authentication> getCurrentAuthentication() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
return Optional.ofNullable(authentication);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.phonebill.common.util;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 검증 유틸리티
|
||||
* 입력값 검증 관련 공통 기능을 제공합니다.
|
||||
*/
|
||||
public class ValidatorUtil {
|
||||
|
||||
// 전화번호 패턴 (010-1234-5678, 01012345678)
|
||||
private static final Pattern PHONE_PATTERN = Pattern.compile("^01[0-9]-?[0-9]{3,4}-?[0-9]{4}$");
|
||||
|
||||
// 이메일 패턴
|
||||
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");
|
||||
|
||||
// 사용자 ID 패턴 (영문, 숫자, 3-20자)
|
||||
private static final Pattern USER_ID_PATTERN = Pattern.compile("^[a-zA-Z0-9]{3,20}$");
|
||||
|
||||
// 비밀번호 패턴 (영문, 숫자, 특수문자 포함 8-20자)
|
||||
private static final Pattern PASSWORD_PATTERN = Pattern.compile("^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?]).{8,20}$");
|
||||
|
||||
/**
|
||||
* 전화번호 형식 검증
|
||||
*/
|
||||
public static boolean isValidPhoneNumber(String phoneNumber) {
|
||||
if (phoneNumber == null || phoneNumber.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return PHONE_PATTERN.matcher(phoneNumber.trim()).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* 이메일 형식 검증
|
||||
*/
|
||||
public static boolean isValidEmail(String email) {
|
||||
if (email == null || email.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return EMAIL_PATTERN.matcher(email.trim()).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 ID 형식 검증
|
||||
*/
|
||||
public static boolean isValidUserId(String userId) {
|
||||
if (userId == null || userId.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return USER_ID_PATTERN.matcher(userId.trim()).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 형식 검증
|
||||
*/
|
||||
public static boolean isValidPassword(String password) {
|
||||
if (password == null || password.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return PASSWORD_PATTERN.matcher(password).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열이 null이거나 비어있는지 검증
|
||||
*/
|
||||
public static boolean isNullOrEmpty(String str) {
|
||||
return str == null || str.trim().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열이 null이거나 비어있지 않은지 검증
|
||||
*/
|
||||
public static boolean isNotNullOrEmpty(String str) {
|
||||
return !isNullOrEmpty(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열 길이 검증
|
||||
*/
|
||||
public static boolean isValidLength(String str, int minLength, int maxLength) {
|
||||
if (str == null) {
|
||||
return minLength == 0;
|
||||
}
|
||||
int length = str.length();
|
||||
return length >= minLength && length <= maxLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* 숫자 문자열 검증
|
||||
*/
|
||||
public static boolean isNumeric(String str) {
|
||||
if (str == null || str.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Long.parseLong(str.trim());
|
||||
return true;
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 양수 검증
|
||||
*/
|
||||
public static boolean isPositiveNumber(String str) {
|
||||
if (!isNumeric(str)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
long number = Long.parseLong(str.trim());
|
||||
return number > 0;
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user