mirror of
https://github.com/hwanny1128/HGZero.git
synced 2026-06-12 22:59:10 +00:00
develop
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'org.springframework.boot'
|
||||
}
|
||||
|
||||
bootJar {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
jar {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Spring Boot
|
||||
api 'org.springframework.boot:spring-boot-starter-web'
|
||||
api 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
api 'org.springframework.boot:spring-boot-starter-security'
|
||||
api 'org.springframework.boot:spring-boot-starter-validation'
|
||||
api 'org.springframework.boot:spring-boot-starter-aop'
|
||||
|
||||
// JWT
|
||||
api "io.jsonwebtoken:jjwt-api:${jjwtVersion}"
|
||||
|
||||
// Utilities
|
||||
api "org.apache.commons:commons-lang3:${commonsLang3Version}"
|
||||
api "commons-io:commons-io:${commonsIoVersion}"
|
||||
|
||||
// Jackson
|
||||
api 'com.fasterxml.jackson.core:jackson-databind'
|
||||
api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.unicorn.hgzero.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.aspectj.lang.annotation.Pointcut;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 로깅 AOP
|
||||
* Controller, Service 메서드 실행 시 로그를 자동으로 기록
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
public class LoggingAspect {
|
||||
|
||||
/**
|
||||
* Controller 메서드 포인트컷
|
||||
*/
|
||||
@Pointcut("within(@org.springframework.web.bind.annotation.RestController *)")
|
||||
public void controllerPointcut() {
|
||||
// Pointcut for all methods in @RestController classes
|
||||
}
|
||||
|
||||
/**
|
||||
* Service 메서드 포인트컷
|
||||
*/
|
||||
@Pointcut("within(@org.springframework.stereotype.Service *)")
|
||||
public void servicePointcut() {
|
||||
// Pointcut for all methods in @Service classes
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller 메서드 실행 로깅
|
||||
*
|
||||
* @param joinPoint 조인 포인트
|
||||
* @return 메서드 실행 결과
|
||||
* @throws Throwable 예외 발생 시
|
||||
*/
|
||||
@Around("controllerPointcut()")
|
||||
public Object logController(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
String className = joinPoint.getSignature().getDeclaringTypeName();
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
Object[] args = joinPoint.getArgs();
|
||||
|
||||
log.info("[Controller] {}.{} 호출 - 파라미터: {}",
|
||||
className, methodName, Arrays.toString(args));
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
Object result = null;
|
||||
try {
|
||||
result = joinPoint.proceed();
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
log.info("[Controller] {}.{} 완료 - 실행시간: {}ms",
|
||||
className, methodName, executionTime);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
log.error("[Controller] {}.{} 실패 - 실행시간: {}ms, 에러: {}",
|
||||
className, methodName, executionTime, e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Service 메서드 실행 로깅
|
||||
*
|
||||
* @param joinPoint 조인 포인트
|
||||
* @return 메서드 실행 결과
|
||||
* @throws Throwable 예외 발생 시
|
||||
*/
|
||||
@Around("servicePointcut()")
|
||||
public Object logService(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
String className = joinPoint.getSignature().getDeclaringTypeName();
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
|
||||
log.debug("[Service] {}.{} 시작", className, methodName);
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
Object result = null;
|
||||
try {
|
||||
result = joinPoint.proceed();
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
log.debug("[Service] {}.{} 완료 - 실행시간: {}ms",
|
||||
className, methodName, executionTime);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
log.error("[Service] {}.{} 실패 - 실행시간: {}ms, 에러: {}",
|
||||
className, methodName, executionTime, e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.unicorn.hgzero.common.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
|
||||
/**
|
||||
* JPA 설정
|
||||
* JPA Auditing 기능을 활성화하여 엔티티의 생성일시, 수정일시를 자동으로 관리
|
||||
*/
|
||||
@Configuration
|
||||
@EnableJpaAuditing
|
||||
public class JpaConfig {
|
||||
// JPA Auditing 활성화
|
||||
// BaseTimeEntity의 @CreatedDate, @LastModifiedDate가 자동으로 동작
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.unicorn.hgzero.common.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* API 응답 공통 포맷
|
||||
* 모든 API 응답에 사용되는 표준 응답 형식
|
||||
*
|
||||
* @param <T> 응답 데이터 타입
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class ApiResponse<T> {
|
||||
|
||||
/**
|
||||
* 응답 상태 (success, error)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 응답 메시지
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 응답 데이터
|
||||
*/
|
||||
private T data;
|
||||
|
||||
/**
|
||||
* 에러 코드 (에러 발생 시)
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 에러 상세 정보 (에러 발생 시)
|
||||
*/
|
||||
private Object details;
|
||||
|
||||
/**
|
||||
* 응답 타임스탬프
|
||||
*/
|
||||
@Builder.Default
|
||||
private LocalDateTime timestamp = LocalDateTime.now();
|
||||
|
||||
/**
|
||||
* 성공 응답 생성
|
||||
*
|
||||
* @param <T> 응답 데이터 타입
|
||||
* @param data 응답 데이터
|
||||
* @return 성공 응답
|
||||
*/
|
||||
public static <T> ApiResponse<T> success(T data) {
|
||||
return ApiResponse.<T>builder()
|
||||
.status("success")
|
||||
.data(data)
|
||||
.timestamp(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 성공 응답 생성 (메시지 포함)
|
||||
*
|
||||
* @param <T> 응답 데이터 타입
|
||||
* @param message 성공 메시지
|
||||
* @param data 응답 데이터
|
||||
* @return 성공 응답
|
||||
*/
|
||||
public static <T> ApiResponse<T> success(String message, T data) {
|
||||
return ApiResponse.<T>builder()
|
||||
.status("success")
|
||||
.message(message)
|
||||
.data(data)
|
||||
.timestamp(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 응답 생성
|
||||
*
|
||||
* @param code 에러 코드
|
||||
* @param message 에러 메시지
|
||||
* @return 에러 응답
|
||||
*/
|
||||
public static ApiResponse<Void> error(String code, String message) {
|
||||
return ApiResponse.<Void>builder()
|
||||
.status("error")
|
||||
.code(code)
|
||||
.message(message)
|
||||
.timestamp(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 응답 생성 (상세 정보 포함)
|
||||
*
|
||||
* @param code 에러 코드
|
||||
* @param message 에러 메시지
|
||||
* @param details 에러 상세 정보
|
||||
* @return 에러 응답
|
||||
*/
|
||||
public static ApiResponse<Void> error(String code, String message, Object details) {
|
||||
return ApiResponse.<Void>builder()
|
||||
.status("error")
|
||||
.code(code)
|
||||
.message(message)
|
||||
.details(details)
|
||||
.timestamp(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.unicorn.hgzero.common.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* JWT 토큰 DTO
|
||||
* Access Token과 Refresh Token을 함께 반환하는 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class JwtTokenDTO {
|
||||
|
||||
/**
|
||||
* Access Token
|
||||
* 단기 인증 토큰 (기본 1시간)
|
||||
*/
|
||||
private String accessToken;
|
||||
|
||||
/**
|
||||
* Refresh Token
|
||||
* 장기 인증 토큰 (기본 7일)
|
||||
*/
|
||||
private String refreshToken;
|
||||
|
||||
/**
|
||||
* Access Token 유효 기간 (초)
|
||||
*/
|
||||
private Long accessTokenValidity;
|
||||
|
||||
/**
|
||||
* Refresh Token 유효 기간 (초)
|
||||
*/
|
||||
private Long refreshTokenValidity;
|
||||
|
||||
/**
|
||||
* 토큰 타입 (Bearer)
|
||||
*/
|
||||
@Builder.Default
|
||||
private String tokenType = "Bearer";
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.unicorn.hgzero.common.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* JWT Token 갱신 요청/응답 DTO
|
||||
* Refresh Token을 사용하여 새로운 Access Token을 발급받을 때 사용
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class JwtTokenRefreshDTO {
|
||||
|
||||
/**
|
||||
* Refresh Token
|
||||
*/
|
||||
private String refreshToken;
|
||||
|
||||
/**
|
||||
* 새로 발급된 Access Token
|
||||
*/
|
||||
private String accessToken;
|
||||
|
||||
/**
|
||||
* Access Token 유효 기간 (초)
|
||||
*/
|
||||
private Long accessTokenValidity;
|
||||
|
||||
/**
|
||||
* 토큰 타입 (Bearer)
|
||||
*/
|
||||
@Builder.Default
|
||||
private String tokenType = "Bearer";
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.unicorn.hgzero.common.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* JWT Token 검증 결과 DTO
|
||||
* 토큰 유효성 검증 결과를 반환하는 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class JwtTokenVerifyDTO {
|
||||
|
||||
/**
|
||||
* 토큰 유효 여부
|
||||
*/
|
||||
private Boolean valid;
|
||||
|
||||
/**
|
||||
* 사용자 ID
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 사용자 이름
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 권한
|
||||
*/
|
||||
private String authority;
|
||||
|
||||
/**
|
||||
* 토큰 만료 여부
|
||||
*/
|
||||
private Boolean expired;
|
||||
|
||||
/**
|
||||
* 에러 메시지 (유효하지 않은 경우)
|
||||
*/
|
||||
private String errorMessage;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.unicorn.hgzero.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;
|
||||
|
||||
/**
|
||||
* 생성일시/수정일시 자동 관리 Entity
|
||||
* 모든 Entity의 기본 클래스로 사용되며, 생성일시와 수정일시를 자동으로 관리
|
||||
*/
|
||||
@Getter
|
||||
@MappedSuperclass
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public abstract class BaseTimeEntity {
|
||||
|
||||
/**
|
||||
* 생성일시
|
||||
* Entity가 처음 생성될 때 자동으로 설정
|
||||
*/
|
||||
@CreatedDate
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 수정일시
|
||||
* Entity가 수정될 때마다 자동으로 갱신
|
||||
*/
|
||||
@LastModifiedDate
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.unicorn.hgzero.common.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 비즈니스 로직 예외
|
||||
* 비즈니스 규칙 위반 또는 비즈니스 로직 처리 중 발생하는 예외
|
||||
*/
|
||||
@Getter
|
||||
public class BusinessException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* 에러 코드
|
||||
*/
|
||||
private final ErrorCode errorCode;
|
||||
|
||||
/**
|
||||
* 에러 상세 정보
|
||||
*/
|
||||
private final Object 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, Object 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 = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.unicorn.hgzero.common.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
/**
|
||||
* 에러 코드 정의
|
||||
* 시스템 전체에서 사용되는 표준 에러 코드
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum ErrorCode {
|
||||
|
||||
// 공통 에러 (1xxx)
|
||||
INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "C001", "잘못된 입력 값입니다."),
|
||||
METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "C002", "허용되지 않은 HTTP 메서드입니다."),
|
||||
ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "C003", "요청한 리소스를 찾을 수 없습니다."),
|
||||
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "C004", "서버 내부 오류가 발생했습니다."),
|
||||
INVALID_TYPE_VALUE(HttpStatus.BAD_REQUEST, "C005", "잘못된 타입의 값입니다."),
|
||||
ACCESS_DENIED(HttpStatus.FORBIDDEN, "C006", "접근 권한이 없습니다."),
|
||||
|
||||
// 인증/인가 에러 (2xxx)
|
||||
AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "A001", "인증에 실패했습니다."),
|
||||
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "A002", "유효하지 않은 토큰입니다."),
|
||||
EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "A003", "만료된 토큰입니다."),
|
||||
REFRESH_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "A004", "Refresh Token을 찾을 수 없습니다."),
|
||||
INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "A005", "유효하지 않은 Refresh Token입니다."),
|
||||
ACCOUNT_LOCKED(HttpStatus.UNAUTHORIZED, "A006", "계정이 잠금 상태입니다."),
|
||||
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "A007", "인증이 필요합니다."),
|
||||
|
||||
// 비즈니스 로직 에러 (3xxx)
|
||||
DUPLICATE_RESOURCE(HttpStatus.CONFLICT, "B001", "이미 존재하는 리소스입니다."),
|
||||
INVALID_STATUS_TRANSITION(HttpStatus.BAD_REQUEST, "B002", "유효하지 않은 상태 전환입니다."),
|
||||
RESOURCE_CONFLICT(HttpStatus.CONFLICT, "B003", "리소스 충돌이 발생했습니다."),
|
||||
OPERATION_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "B004", "허용되지 않은 작업입니다."),
|
||||
BUSINESS_RULE_VIOLATION(HttpStatus.BAD_REQUEST, "B005", "비즈니스 규칙 위반입니다."),
|
||||
|
||||
// 외부 시스템 에러 (4xxx)
|
||||
EXTERNAL_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E001", "외부 API 호출 중 오류가 발생했습니다."),
|
||||
DATABASE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E002", "데이터베이스 오류가 발생했습니다."),
|
||||
CACHE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E003", "캐시 오류가 발생했습니다."),
|
||||
MESSAGE_QUEUE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E004", "메시지 큐 오류가 발생했습니다."),
|
||||
STORAGE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E005", "스토리지 오류가 발생했습니다.");
|
||||
|
||||
/**
|
||||
* HTTP 상태 코드
|
||||
*/
|
||||
private final HttpStatus httpStatus;
|
||||
|
||||
/**
|
||||
* 에러 코드
|
||||
*/
|
||||
private final String code;
|
||||
|
||||
/**
|
||||
* 에러 메시지
|
||||
*/
|
||||
private final String message;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.unicorn.hgzero.common.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 인프라스트럭처 예외
|
||||
* 외부 시스템(데이터베이스, 캐시, 메시지 큐, 스토리지 등) 연동 중 발생하는 예외
|
||||
*/
|
||||
@Getter
|
||||
public class InfraException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* 에러 코드
|
||||
*/
|
||||
private final ErrorCode errorCode;
|
||||
|
||||
/**
|
||||
* 에러 상세 정보
|
||||
*/
|
||||
private final Object 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, Object 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 = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.unicorn.hgzero.common.util;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
/**
|
||||
* 날짜/시간 유틸리티 클래스
|
||||
* 날짜와 시간 관련 공통 기능을 제공
|
||||
*/
|
||||
public class DateUtil {
|
||||
|
||||
/**
|
||||
* 기본 날짜 포맷 (yyyy-MM-dd)
|
||||
*/
|
||||
public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
/**
|
||||
* 기본 날짜시간 포맷 (yyyy-MM-dd HH:mm:ss)
|
||||
*/
|
||||
public static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
/**
|
||||
* ISO 8601 날짜시간 포맷 (yyyy-MM-dd'T'HH:mm:ss)
|
||||
*/
|
||||
public static final DateTimeFormatter ISO_DATETIME_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
||||
|
||||
private DateUtil() {
|
||||
// Utility class - prevent instantiation
|
||||
}
|
||||
|
||||
/**
|
||||
* LocalDate를 문자열로 변환
|
||||
*
|
||||
* @param date 변환할 날짜
|
||||
* @return 포맷된 날짜 문자열 (yyyy-MM-dd)
|
||||
*/
|
||||
public static String format(LocalDate date) {
|
||||
return date != null ? date.format(DATE_FORMATTER) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* LocalDateTime을 문자열로 변환
|
||||
*
|
||||
* @param dateTime 변환할 날짜시간
|
||||
* @return 포맷된 날짜시간 문자열 (yyyy-MM-dd HH:mm:ss)
|
||||
*/
|
||||
public static String format(LocalDateTime dateTime) {
|
||||
return dateTime != null ? dateTime.format(DATETIME_FORMATTER) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* LocalDateTime을 커스텀 포맷으로 변환
|
||||
*
|
||||
* @param dateTime 변환할 날짜시간
|
||||
* @param formatter 날짜시간 포맷터
|
||||
* @return 포맷된 날짜시간 문자열
|
||||
*/
|
||||
public static String format(LocalDateTime dateTime, DateTimeFormatter formatter) {
|
||||
return dateTime != null ? dateTime.format(formatter) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열을 LocalDate로 변환
|
||||
*
|
||||
* @param dateString 날짜 문자열 (yyyy-MM-dd)
|
||||
* @return LocalDate 객체
|
||||
*/
|
||||
public static LocalDate parseDate(String dateString) {
|
||||
return dateString != null ? LocalDate.parse(dateString, DATE_FORMATTER) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열을 LocalDateTime으로 변환
|
||||
*
|
||||
* @param dateTimeString 날짜시간 문자열 (yyyy-MM-dd HH:mm:ss)
|
||||
* @return LocalDateTime 객체
|
||||
*/
|
||||
public static LocalDateTime parseDateTime(String dateTimeString) {
|
||||
return dateTimeString != null ? LocalDateTime.parse(dateTimeString, DATETIME_FORMATTER) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 두 날짜 사이의 일수 계산
|
||||
*
|
||||
* @param startDate 시작 날짜
|
||||
* @param endDate 종료 날짜
|
||||
* @return 일수 차이
|
||||
*/
|
||||
public static long daysBetween(LocalDate startDate, LocalDate endDate) {
|
||||
return ChronoUnit.DAYS.between(startDate, endDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 두 날짜시간 사이의 시간 계산 (시간 단위)
|
||||
*
|
||||
* @param startDateTime 시작 날짜시간
|
||||
* @param endDateTime 종료 날짜시간
|
||||
* @return 시간 차이
|
||||
*/
|
||||
public static long hoursBetween(LocalDateTime startDateTime, LocalDateTime endDateTime) {
|
||||
return ChronoUnit.HOURS.between(startDateTime, endDateTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 두 날짜시간 사이의 시간 계산 (분 단위)
|
||||
*
|
||||
* @param startDateTime 시작 날짜시간
|
||||
* @param endDateTime 종료 날짜시간
|
||||
* @return 분 차이
|
||||
*/
|
||||
public static long minutesBetween(LocalDateTime startDateTime, LocalDateTime endDateTime) {
|
||||
return ChronoUnit.MINUTES.between(startDateTime, endDateTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 날짜 반환
|
||||
*
|
||||
* @return 현재 날짜
|
||||
*/
|
||||
public static LocalDate now() {
|
||||
return LocalDate.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 날짜시간 반환
|
||||
*
|
||||
* @return 현재 날짜시간
|
||||
*/
|
||||
public static LocalDateTime nowDateTime() {
|
||||
return LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package com.unicorn.hgzero.common.util;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* 문자열 유틸리티 클래스
|
||||
* 문자열 관련 공통 기능을 제공
|
||||
*/
|
||||
public class StringUtil {
|
||||
|
||||
private StringUtil() {
|
||||
// Utility class - prevent instantiation
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열이 비어있는지 확인
|
||||
*
|
||||
* @param str 확인할 문자열
|
||||
* @return 비어있으면 true, 아니면 false
|
||||
*/
|
||||
public static boolean isEmpty(String str) {
|
||||
return !StringUtils.hasText(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열이 비어있지 않은지 확인
|
||||
*
|
||||
* @param str 확인할 문자열
|
||||
* @return 비어있지 않으면 true, 아니면 false
|
||||
*/
|
||||
public static boolean isNotEmpty(String str) {
|
||||
return StringUtils.hasText(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열 앞뒤 공백 제거
|
||||
* null-safe 처리
|
||||
*
|
||||
* @param str 입력 문자열
|
||||
* @return 공백이 제거된 문자열 (null인 경우 null 반환)
|
||||
*/
|
||||
public static String trim(String str) {
|
||||
return str != null ? str.trim() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열 앞뒤 공백 제거
|
||||
* null인 경우 빈 문자열 반환
|
||||
*
|
||||
* @param str 입력 문자열
|
||||
* @return 공백이 제거된 문자열 (null인 경우 빈 문자열 반환)
|
||||
*/
|
||||
public static String trimToEmpty(String str) {
|
||||
return str != null ? str.trim() : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열이 null 또는 빈 문자열인 경우 기본값 반환
|
||||
*
|
||||
* @param str 입력 문자열
|
||||
* @param defaultValue 기본값
|
||||
* @return 문자열이 비어있으면 기본값, 아니면 입력 문자열
|
||||
*/
|
||||
public static String defaultIfEmpty(String str, String defaultValue) {
|
||||
return isEmpty(str) ? defaultValue : str;
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열 마스킹
|
||||
* 이메일, 전화번호 등 민감한 정보를 부분적으로 마스킹
|
||||
*
|
||||
* @param str 마스킹할 문자열
|
||||
* @param visibleStart 앞에서 보일 문자 수
|
||||
* @param visibleEnd 뒤에서 보일 문자 수
|
||||
* @param maskChar 마스킹 문자
|
||||
* @return 마스킹된 문자열
|
||||
*/
|
||||
public static String mask(String str, int visibleStart, int visibleEnd, char maskChar) {
|
||||
if (isEmpty(str) || str.length() <= (visibleStart + visibleEnd)) {
|
||||
return str;
|
||||
}
|
||||
|
||||
int maskLength = str.length() - visibleStart - visibleEnd;
|
||||
String maskedPart = String.valueOf(maskChar).repeat(maskLength);
|
||||
|
||||
return str.substring(0, visibleStart) +
|
||||
maskedPart +
|
||||
str.substring(str.length() - visibleEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* 이메일 마스킹
|
||||
* 예: test@example.com -> te**@example.com
|
||||
*
|
||||
* @param email 마스킹할 이메일
|
||||
* @return 마스킹된 이메일
|
||||
*/
|
||||
public static String maskEmail(String email) {
|
||||
if (isEmpty(email) || !email.contains("@")) {
|
||||
return email;
|
||||
}
|
||||
|
||||
String[] parts = email.split("@");
|
||||
String localPart = parts[0];
|
||||
String domain = parts[1];
|
||||
|
||||
if (localPart.length() <= 2) {
|
||||
return localPart + "@" + domain;
|
||||
}
|
||||
|
||||
String maskedLocal = localPart.substring(0, 2) +
|
||||
"*".repeat(localPart.length() - 2);
|
||||
|
||||
return maskedLocal + "@" + domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 전화번호 마스킹
|
||||
* 예: 010-1234-5678 -> 010-****-5678
|
||||
*
|
||||
* @param phone 마스킹할 전화번호
|
||||
* @return 마스킹된 전화번호
|
||||
*/
|
||||
public static String maskPhone(String phone) {
|
||||
if (isEmpty(phone)) {
|
||||
return phone;
|
||||
}
|
||||
|
||||
// 하이픈 제거
|
||||
String cleanPhone = phone.replaceAll("-", "");
|
||||
|
||||
if (cleanPhone.length() < 8) {
|
||||
return phone;
|
||||
}
|
||||
|
||||
// 뒤 4자리만 보이도록 마스킹
|
||||
String masked = "*".repeat(cleanPhone.length() - 4) +
|
||||
cleanPhone.substring(cleanPhone.length() - 4);
|
||||
|
||||
// 원본 형식에 따라 하이픈 추가
|
||||
if (phone.contains("-")) {
|
||||
if (cleanPhone.length() == 10) {
|
||||
return masked.substring(0, 3) + "-" +
|
||||
masked.substring(3, 6) + "-" +
|
||||
masked.substring(6);
|
||||
} else if (cleanPhone.length() == 11) {
|
||||
return masked.substring(0, 3) + "-" +
|
||||
masked.substring(3, 7) + "-" +
|
||||
masked.substring(7);
|
||||
}
|
||||
}
|
||||
|
||||
return masked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Camel Case를 Snake Case로 변환
|
||||
* 예: userName -> user_name
|
||||
*
|
||||
* @param camelCase Camel Case 문자열
|
||||
* @return Snake Case 문자열
|
||||
*/
|
||||
public static String toSnakeCase(String camelCase) {
|
||||
if (isEmpty(camelCase)) {
|
||||
return camelCase;
|
||||
}
|
||||
|
||||
return camelCase.replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Snake Case를 Camel Case로 변환
|
||||
* 예: user_name -> userName
|
||||
*
|
||||
* @param snakeCase Snake Case 문자열
|
||||
* @return Camel Case 문자열
|
||||
*/
|
||||
public static String toCamelCase(String snakeCase) {
|
||||
if (isEmpty(snakeCase)) {
|
||||
return snakeCase;
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean nextUpper = false;
|
||||
|
||||
for (char c : snakeCase.toCharArray()) {
|
||||
if (c == '_') {
|
||||
nextUpper = true;
|
||||
} else {
|
||||
result.append(nextUpper ? Character.toUpperCase(c) : c);
|
||||
nextUpper = false;
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user