Skip to content
Merged
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

plugins {
id 'java'
id 'org.springframework.boot' version '3.5.7'
Expand Down Expand Up @@ -144,4 +145,4 @@ configurations {

clean {
delete file(querydslDir)
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.earlyexpress.slack_service;
package com.early_express.slack_service;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.early_express.slack_service.global.common.dto;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Getter;

import java.util.ArrayList;
import java.util.List;

@Getter
public class PageInfo {
private final int page;
private final int size;
private final long totalElements;
private final int totalPages;
private final int numberOfElements;
private final boolean first;
private final boolean last;
private final boolean hasNext;
private final boolean hasPrevious;
private final boolean empty;
private final List<SortInfo> sort;

@JsonCreator
@Builder
private PageInfo(
@JsonProperty("page") int page,
@JsonProperty("size") int size,
@JsonProperty("totalElements") long totalElements,
@JsonProperty("totalPages") int totalPages,
@JsonProperty("numberOfElements") int numberOfElements,
@JsonProperty("sort") List<SortInfo> sort
) {
validateParameters(page, size, totalElements, totalPages);

this.page = page;
this.size = size;
this.totalElements = totalElements;
this.totalPages = totalPages;
this.numberOfElements = numberOfElements;
this.first = (page == 0);
this.last = (page == totalPages - 1) || (totalPages == 0);
this.hasNext = page < totalPages - 1;
this.hasPrevious = page > 0;
this.empty = numberOfElements == 0;
this.sort = sort != null ? new ArrayList<>(sort) : new ArrayList<>();
}

// 정렬 정보 없이 생성 (기존 호환성 유지)
public static PageInfo of(int page, int size, long totalElements,
int totalPages, int numberOfElements) {
return PageInfo.builder()
.page(page)
.size(size)
.totalElements(totalElements)
.totalPages(totalPages)
.numberOfElements(numberOfElements)
.build();
}

// 정렬 정보 포함하여 생성
public static PageInfo of(int page, int size, long totalElements,
int totalPages, int numberOfElements, List<SortInfo> sort) {
return PageInfo.builder()
.page(page)
.size(size)
.totalElements(totalElements)
.totalPages(totalPages)
.numberOfElements(numberOfElements)
.sort(sort)
.build();
}

private void validateParameters(int page, int size, long totalElements, int totalPages) {
if (page < 0) {
throw new IllegalArgumentException("page는 0보다 크거나 같아야 됩니다.");
}
if (size <= 0) {
throw new IllegalArgumentException("size는 0보다 커야합니다.");
}
if (totalElements < 0) {
throw new IllegalArgumentException("totalElements는 0보다 크거나 같아야 합니다.");
}
if (totalPages < 0) {
throw new IllegalArgumentException("totalPages는 0보다 크거나 같아야 합니다.");
}
}


/**
* 정렬 정보를 담는 내부 클래스
*/
@Getter
public static class SortInfo {
private final String property;
private final Direction direction;
private final boolean ignoreCase;

@JsonCreator
@Builder
private SortInfo(
@JsonProperty("property") String property,
@JsonProperty("direction") Direction direction,
@JsonProperty("ignoreCase") boolean ignoreCase
) {
this.property = property;
this.direction = direction;
this.ignoreCase = ignoreCase;
}

public enum Direction {
ASC, DESC
}

public static SortInfo of(String property, Direction direction) {
return SortInfo.builder()
.property(property)
.direction(direction)
.ignoreCase(false)
.build();
}

public static SortInfo of(String property, Direction direction, boolean ignoreCase) {
return SortInfo.builder()
.property(property)
.direction(direction)
.ignoreCase(ignoreCase)
.build();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.early_express.slack_service.global.common.utils;


import com.early_express.slack_service.global.common.dto.PageInfo;
import com.early_express.slack_service.global.presentation.dto.PageResponse;
import com.early_express.slack_service.global.presentation.exception.GlobalErrorCode;
import com.early_express.slack_service.global.presentation.exception.GlobalException;
import org.springframework.data.domain.Page;

import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

public class PageUtils {

private PageUtils() {
// 유틸리티 클래스 인스턴스화 방지
}

/**
* Spring Data Page를 PageResponse로 변경
*/
public static <T> PageResponse<T> toPageResponse(Page<T> page) {
validatePage(page);

PageInfo pageInfo = createPageInfo(page);
return PageResponse.of(page.getContent(), pageInfo);
}

/**
* Spring Data Page를 매퍼 함수를 사용하여 PageResponse로 변환
* 엔티티 -> DTO 변환 시 사용
*/
public static <T, R> PageResponse<R> toPageResponse(Page<T> page, Function<T, R> mapper) {
validatePage(page);
validateMapper(mapper);

List<R> mappedContent = page.getContent().stream()
.map(mapper)
.collect(Collectors.toList());

PageInfo pageInfo = createPageInfo(page);
return PageResponse.of(mappedContent, pageInfo);
}

private static <T> PageInfo createPageInfo(Page<T> page) {
// 정렬 정보 추출
List<PageInfo.SortInfo> sortInfos = page.getSort().stream()
.map(order -> PageInfo.SortInfo.of(
order.getProperty(),
order.isAscending() ? PageInfo.SortInfo.Direction.ASC : PageInfo.SortInfo.Direction.DESC,
order.isIgnoreCase()
))
.collect(Collectors.toList());

// 정렬 정보 포함하여 PageInfo 생성
return PageInfo.of(
page.getNumber(),
page.getSize(),
page.getTotalElements(),
page.getTotalPages(),
page.getNumberOfElements(),
sortInfos
);
}

private static void validatePage(Page<?> page) {
if (page == null) {
throw new PageUtilException(GlobalErrorCode.INVALID_INPUT_VALUE, "페이지 객체는 null일 수 없습니다.");
}
}

private static void validateMapper(Function<?, ?> mapper) {
if (mapper == null) {
throw new PageUtilException(GlobalErrorCode.INVALID_INPUT_VALUE, "매퍼 함수는 null일 수 없습니다.");
}
}

/**
* PageUtils 전용 예외 클래스
*/
public static class PageUtilException extends GlobalException {
public PageUtilException(GlobalErrorCode errorCode, String message) {
super(errorCode, message);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.early_express.slack_service.global.common.utils;

import java.util.UUID;

/**
* UUID 생성 유틸리티
* 표준 36자 UUID만 생성
* */
public class UuidUtils {

private UuidUtils() {
throw new AssertionError("유틸리티 클래스는 인스턴스화 할 수 없습니다.");
}

public static String generate() {
return UUID.randomUUID().toString();
}

public static boolean isValid(String uuid) {
if (uuid == null) {
return false;
}
try {
UUID.fromString(uuid);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.early_express.slack_service.global.config;

import io.github.cdimascio.dotenv.Dotenv;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class EnvConfig {

@PostConstruct
public void init() {
// 환경별 .env 파일 로드
String environment = System.getProperty("spring.profiles.active", "local");

// 기본 .env 파일 로드
Dotenv dotenv = Dotenv.configure()
.directory("./")
.filename(".env")
.ignoreIfMalformed()
.ignoreIfMissing()
.load();

// 환경별 .env 파일 추가 로드
Dotenv envSpecific = Dotenv.configure()
.directory("./")
.filename(".env." + environment)
.ignoreIfMalformed()
.ignoreIfMissing()
.load();

// 기본 .env 설정 적용
dotenv.entries().forEach(entry -> {
if (System.getProperty(entry.getKey()) == null) {
System.setProperty(entry.getKey(), entry.getValue());
}
});

// 환경별 .env 설정으로 덮어쓰기
envSpecific.entries().forEach(entry -> {
System.setProperty(entry.getKey(), entry.getValue());
});

log.info("Environment configuration loaded: {}", environment);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.early_express.slack_service.global.config;

import feign.Logger;
import feign.Request;
import feign.Retryer;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Slf4j
@Configuration
@EnableFeignClients(basePackages = "com.early_express.default_server")
public class FeignConfig {

/**
* Feign 로깅 레벨 설정
*/
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.BASIC;
}

/**
* Request 옵션 설정
*/
@Bean
public Request.Options requestOptions() {
return new Request.Options(
10, TimeUnit.SECONDS, // connectTimeout
30, TimeUnit.SECONDS, // readTimeout
true // followRedirects
);
}

/**
* 재시도 설정
*/
@Bean
public Retryer retryer() {
return new Retryer.Default(
100, // 재시도 간격 (ms)
1000, // 최대 재시도 간격 (ms)
3 // 최대 재시도 횟수
);
}

/**
* 에러 디코더
*/
@Bean
public ErrorDecoder errorDecoder() {
return new ErrorDecoder.Default();
}
}
Loading