This commit is contained in:
djeon
2025-10-23 14:55:33 +09:00
parent 41d57e7399
commit 98ede67f62
109 changed files with 8633 additions and 0 deletions
+32
View File
@@ -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();
}
}