From cc4261a28d5348d5f58c28bf0f1cf39dd93dff4a Mon Sep 17 00:00:00 2001 From: vsuminv Date: Tue, 25 Nov 2025 11:15:05 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat(domain)=20:=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=8A=AC=EB=9E=99?= =?UTF-8?q?=20=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=97=B0=EB=8F=99=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SlackServiceApplication.java | 2 +- .../global/common/dto/PageInfo.java | 132 ++++++++++++++ .../global/common/utils/PageUtils.java | 87 ++++++++++ .../global/common/utils/UuidUtils.java | 30 ++++ .../global/config/EnvConfig.java | 47 +++++ .../global/config/FeignConfig.java | 58 +++++++ .../global/config/JpaConfig.java | 27 +++ .../global/config/KafkaConfig.java | 85 +++++++++ .../global/config/SecurityConfig.java | 109 ++++++++++++ .../global/config/SwaggerConfig.java | 34 ++++ .../global/config/WebClientConfig.java | 102 +++++++++++ .../infrastructure/entity/BaseEntity.java | 113 ++++++++++++ .../SwaggerRedirectController.java | 21 +++ .../global/presentation/TestController.java | 20 +++ .../global/presentation/dto/ApiResponse.java | 86 ++++++++++ .../presentation/dto/ErrorResponse.java | 61 +++++++ .../global/presentation/dto/PageResponse.java | 33 ++++ .../presentation/exception/AuthErrorCode.java | 20 +++ .../presentation/exception/ErrorCode.java | 23 +++ .../exception/GlobalErrorCode.java | 54 ++++++ .../exception/GlobalException.java | 61 +++++++ .../exception/GlobalExceptionHandler.java | 162 ++++++++++++++++++ .../slack/domain/MessageSend.java | 7 + .../slack/domain/entity/MessageType.java | 6 + .../slack/domain/entity/Slack.java | 37 ++++ .../slack/domain/entity/SlackStatus.java | 7 + .../slack/SlackMessageSend.java | 68 ++++++++ .../SlackServiceApplicationTests.java | 27 +++ .../HubRouteServiceApplicationTests.java | 13 -- 29 files changed, 1518 insertions(+), 14 deletions(-) rename src/main/java/com/{earlyexpress => early_express}/slack_service/SlackServiceApplication.java (87%) create mode 100644 src/main/java/com/early_express/slack_service/global/common/dto/PageInfo.java create mode 100644 src/main/java/com/early_express/slack_service/global/common/utils/PageUtils.java create mode 100644 src/main/java/com/early_express/slack_service/global/common/utils/UuidUtils.java create mode 100644 src/main/java/com/early_express/slack_service/global/config/EnvConfig.java create mode 100644 src/main/java/com/early_express/slack_service/global/config/FeignConfig.java create mode 100644 src/main/java/com/early_express/slack_service/global/config/JpaConfig.java create mode 100644 src/main/java/com/early_express/slack_service/global/config/KafkaConfig.java create mode 100644 src/main/java/com/early_express/slack_service/global/config/SecurityConfig.java create mode 100644 src/main/java/com/early_express/slack_service/global/config/SwaggerConfig.java create mode 100644 src/main/java/com/early_express/slack_service/global/config/WebClientConfig.java create mode 100644 src/main/java/com/early_express/slack_service/global/infrastructure/entity/BaseEntity.java create mode 100644 src/main/java/com/early_express/slack_service/global/presentation/SwaggerRedirectController.java create mode 100644 src/main/java/com/early_express/slack_service/global/presentation/TestController.java create mode 100644 src/main/java/com/early_express/slack_service/global/presentation/dto/ApiResponse.java create mode 100644 src/main/java/com/early_express/slack_service/global/presentation/dto/ErrorResponse.java create mode 100644 src/main/java/com/early_express/slack_service/global/presentation/dto/PageResponse.java create mode 100644 src/main/java/com/early_express/slack_service/global/presentation/exception/AuthErrorCode.java create mode 100644 src/main/java/com/early_express/slack_service/global/presentation/exception/ErrorCode.java create mode 100644 src/main/java/com/early_express/slack_service/global/presentation/exception/GlobalErrorCode.java create mode 100644 src/main/java/com/early_express/slack_service/global/presentation/exception/GlobalException.java create mode 100644 src/main/java/com/early_express/slack_service/global/presentation/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/early_express/slack_service/slack/domain/MessageSend.java create mode 100644 src/main/java/com/early_express/slack_service/slack/domain/entity/MessageType.java create mode 100644 src/main/java/com/early_express/slack_service/slack/domain/entity/Slack.java create mode 100644 src/main/java/com/early_express/slack_service/slack/domain/entity/SlackStatus.java create mode 100644 src/main/java/com/early_express/slack_service/slack/infrastructure/slack/SlackMessageSend.java create mode 100644 src/test/java/com/early_express/slack_service/SlackServiceApplicationTests.java delete mode 100644 src/test/java/com/earlyexpress/hubRoute_service/HubRouteServiceApplicationTests.java diff --git a/src/main/java/com/earlyexpress/slack_service/SlackServiceApplication.java b/src/main/java/com/early_express/slack_service/SlackServiceApplication.java similarity index 87% rename from src/main/java/com/earlyexpress/slack_service/SlackServiceApplication.java rename to src/main/java/com/early_express/slack_service/SlackServiceApplication.java index 367d50f..ce0ab63 100644 --- a/src/main/java/com/earlyexpress/slack_service/SlackServiceApplication.java +++ b/src/main/java/com/early_express/slack_service/SlackServiceApplication.java @@ -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; diff --git a/src/main/java/com/early_express/slack_service/global/common/dto/PageInfo.java b/src/main/java/com/early_express/slack_service/global/common/dto/PageInfo.java new file mode 100644 index 0000000..3fc645e --- /dev/null +++ b/src/main/java/com/early_express/slack_service/global/common/dto/PageInfo.java @@ -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 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 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 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(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/early_express/slack_service/global/common/utils/PageUtils.java b/src/main/java/com/early_express/slack_service/global/common/utils/PageUtils.java new file mode 100644 index 0000000..fcd4703 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/global/common/utils/PageUtils.java @@ -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 PageResponse toPageResponse(Page page) { + validatePage(page); + + PageInfo pageInfo = createPageInfo(page); + return PageResponse.of(page.getContent(), pageInfo); + } + + /** + * Spring Data Page를 매퍼 함수를 사용하여 PageResponse로 변환 + * 엔티티 -> DTO 변환 시 사용 + */ + public static PageResponse toPageResponse(Page page, Function mapper) { + validatePage(page); + validateMapper(mapper); + + List mappedContent = page.getContent().stream() + .map(mapper) + .collect(Collectors.toList()); + + PageInfo pageInfo = createPageInfo(page); + return PageResponse.of(mappedContent, pageInfo); + } + + private static PageInfo createPageInfo(Page page) { + // 정렬 정보 추출 + List 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); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/early_express/slack_service/global/common/utils/UuidUtils.java b/src/main/java/com/early_express/slack_service/global/common/utils/UuidUtils.java new file mode 100644 index 0000000..9022463 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/global/common/utils/UuidUtils.java @@ -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; + } + } +} diff --git a/src/main/java/com/early_express/slack_service/global/config/EnvConfig.java b/src/main/java/com/early_express/slack_service/global/config/EnvConfig.java new file mode 100644 index 0000000..7028528 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/global/config/EnvConfig.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/early_express/slack_service/global/config/FeignConfig.java b/src/main/java/com/early_express/slack_service/global/config/FeignConfig.java new file mode 100644 index 0000000..32daf15 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/global/config/FeignConfig.java @@ -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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/early_express/slack_service/global/config/JpaConfig.java b/src/main/java/com/early_express/slack_service/global/config/JpaConfig.java new file mode 100644 index 0000000..524300e --- /dev/null +++ b/src/main/java/com/early_express/slack_service/global/config/JpaConfig.java @@ -0,0 +1,27 @@ +package com.early_express.slack_service.global.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@Configuration +@EnableJpaAuditing +@EnableTransactionManagement +public class JpaConfig { + + @PersistenceContext + private EntityManager entityManager; + + /** + * QueryDSL을 위한 JPAQueryFactory Bean 등록 + * 모든 Repository에서 주입받아 사용 가능 + */ + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} \ No newline at end of file diff --git a/src/main/java/com/early_express/slack_service/global/config/KafkaConfig.java b/src/main/java/com/early_express/slack_service/global/config/KafkaConfig.java new file mode 100644 index 0000000..d6f1b7b --- /dev/null +++ b/src/main/java/com/early_express/slack_service/global/config/KafkaConfig.java @@ -0,0 +1,85 @@ +package com.early_express.slack_service.global.config; + +import org.apache.kafka.clients.admin.NewTopic; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.config.TopicBuilder; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.core.ProducerFactory; +import org.springframework.kafka.support.converter.RecordMessageConverter; +import org.springframework.kafka.support.converter.StringJsonMessageConverter; +import org.springframework.kafka.support.serializer.JsonSerializer; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +@EnableKafka +public class KafkaConfig { + + @Value("${spring.application.name}") + private String applicationName; + + @Value("${spring.kafka.bootstrap-servers}") + private String bootstrapServers; + + @Bean + public RecordMessageConverter converter() { + return new StringJsonMessageConverter(); + } + + @Bean + public ProducerFactory producerFactory() { + Map configProps = new HashMap<>(); + configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); + + configProps.put(ProducerConfig.ACKS_CONFIG, "all"); + configProps.put(ProducerConfig.RETRIES_CONFIG, 3); + configProps.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true); + configProps.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 5); + + return new DefaultKafkaProducerFactory<>(configProps); + } + + @Bean + public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); + } + + // 기본 토픽 생성 예제 + // 이벤트 토픽 - 도메인 이벤트 발행용 + @Bean + public NewTopic applicationEventsTopic() { + return TopicBuilder.name(applicationName + "-events") + .partitions(3) + .replicas(3) + .config("min.insync.replicas", "2") + .build(); + } + + // 커맨드 토픽 - 명령 처리용 + @Bean + public NewTopic applicationCommandsTopic() { + return TopicBuilder.name(applicationName + "-commands") + .partitions(3) + .replicas(3) + .config("min.insync.replicas", "2") + .build(); + } + + // DLT(Dead Letter Topic) - 처리 실패한 메시지 보관 + @Bean + public NewTopic applicationDeadLetterTopic() { + return TopicBuilder.name(applicationName + "-dlt") + .partitions(1) + .replicas(3) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/early_express/slack_service/global/config/SecurityConfig.java b/src/main/java/com/early_express/slack_service/global/config/SecurityConfig.java new file mode 100644 index 0000000..6ea4d1c --- /dev/null +++ b/src/main/java/com/early_express/slack_service/global/config/SecurityConfig.java @@ -0,0 +1,109 @@ +package com.early_express.slack_service.global.config; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; +import java.util.List; + +@Slf4j +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + /** + * Security Filter Chain 설정 + * 현재는 모든 요청을 허용하도록 설정 (개발 초기 단계) + */ + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + // CSRF 비활성화 (REST API이므로) + .csrf(AbstractHttpConfigurer::disable) + + // CORS 설정 + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + + // ⭐ H2 Console을 위한 Headers 설정 추가 + .headers(headers -> headers + .frameOptions(frameOptions -> frameOptions.sameOrigin()) + ) + + // 세션 사용하지 않음 (Stateless) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + + // 모든 요청 허용 (개발 단계) + // TODO: 실제 운영 환경에서는 적절한 권한 설정 필요 + .authorizeHttpRequests(auth -> auth + // Actuator endpoints + .requestMatchers("/actuator/**").permitAll() + // Swagger UI + .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() + // Health check + .requestMatchers("/health", "/info").permitAll() + // ⭐ H2 Console 추가 + .requestMatchers("/h2-console/**").permitAll() + // 모든 요청 허용 (개발 초기 단계) + .anyRequest().permitAll() + ); + + log.info("Security configuration completed - All endpoints are currently open (including H2 Console)"); + + return http.build(); + } + + /** + * Password Encoder + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + /** + * CORS 설정 + */ + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + // 허용할 Origin 설정 + configuration.setAllowedOriginPatterns(List.of("*")); + + // 허용할 HTTP Method + configuration.setAllowedMethods(Arrays.asList( + "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS" + )); + + // 허용할 Header + configuration.setAllowedHeaders(List.of("*")); + + // 인증 정보 포함 허용 + configuration.setAllowCredentials(true); + + // preflight 캐시 시간 + configuration.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + + return source; + } +} \ No newline at end of file diff --git a/src/main/java/com/early_express/slack_service/global/config/SwaggerConfig.java b/src/main/java/com/early_express/slack_service/global/config/SwaggerConfig.java new file mode 100644 index 0000000..85e6354 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/global/config/SwaggerConfig.java @@ -0,0 +1,34 @@ +package com.early_express.slack_service.global.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +/** + * Swagger/OpenAPI 설정 (프록시 환경용) + */ +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .info(new Info() + .title("Default Server API Documentation") + .description("Early Express Default Server REST API") + .version("1.0.0") + .contact(new Contact() + .name("Early Express Team") + .email("dev@early-express.com"))) + .servers(List.of( + new Server() + .url("/") + .description("현재 서버 (상대 경로)") + )); + } +} \ No newline at end of file diff --git a/src/main/java/com/early_express/slack_service/global/config/WebClientConfig.java b/src/main/java/com/early_express/slack_service/global/config/WebClientConfig.java new file mode 100644 index 0000000..b37ef0b --- /dev/null +++ b/src/main/java/com/early_express/slack_service/global/config/WebClientConfig.java @@ -0,0 +1,102 @@ +package com.early_express.slack_service.global.config; + + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeStrategies; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@Slf4j +@Configuration +public class WebClientConfig { + + @Value("${webclient.max-memory-size:10485760}") // 10MB + private int maxMemorySize; + + /** + * 기본 WebClient - 외부 API 호출용 + */ + @Bean + public WebClient webClient() { + return createWebClient() + .baseUrl("") + .build(); + } + + /** + * LoadBalanced WebClient - 내부 MSA 서비스 호출용 + */ + @Bean + @LoadBalanced + public WebClient.Builder loadBalancedWebClientBuilder() { + return createWebClient(); + } + + /** + * WebClient Builder 생성 + */ + private WebClient.Builder createWebClient() { + // Exchange Strategies 설정 (메모리 제한) + ExchangeStrategies strategies = ExchangeStrategies.builder() + .codecs(configurer -> { + configurer.defaultCodecs().maxInMemorySize(maxMemorySize); + configurer.defaultCodecs().enableLoggingRequestDetails(true); + }) + .build(); + + return WebClient.builder() + .exchangeStrategies(strategies) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .filter(logRequest()) + .filter(logResponse()) + .filter(handleError()); + } + + /** + * Request 로깅 필터 + */ + private ExchangeFilterFunction logRequest() { + return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> { + log.debug("Request: {} {}", clientRequest.method(), clientRequest.url()); + clientRequest.headers().forEach((name, values) -> + values.forEach(value -> log.debug("Request Header: {}={}", name, value)) + ); + return Mono.just(clientRequest); + }); + } + + /** + * Response 로깅 필터 + */ + private ExchangeFilterFunction logResponse() { + return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { + log.debug("Response Status: {}", clientResponse.statusCode()); + clientResponse.headers().asHttpHeaders().forEach((name, values) -> + values.forEach(value -> log.debug("Response Header: {}={}", name, value)) + ); + return Mono.just(clientResponse); + }); + } + + /** + * 에러 처리 필터 + */ + private ExchangeFilterFunction handleError() { + return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { + if (clientResponse.statusCode().is5xxServerError()) { + log.error("Server error: {}", clientResponse.statusCode()); + } else if (clientResponse.statusCode().is4xxClientError()) { + log.warn("Client error: {}", clientResponse.statusCode()); + } + return Mono.just(clientResponse); + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/early_express/slack_service/global/infrastructure/entity/BaseEntity.java b/src/main/java/com/early_express/slack_service/global/infrastructure/entity/BaseEntity.java new file mode 100644 index 0000000..11a39b8 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/global/infrastructure/entity/BaseEntity.java @@ -0,0 +1,113 @@ +package com.early_express.slack_service.global.infrastructure.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +/** + * 기본 엔티티 - 감사(Audit) 정보 관리 + * - createdAt: 생성 시간 + * - createdBy: 생성자 ID + * - updatedAt: 수정 시간 + * - updatedBy: 수정자 ID + * - deletedAt: 삭제 시간 + * - deletedBy: 삭제자 ID + * - isDeleted: 삭제 여부 (Soft Delete) + * + * 사용 예시: + *
+ * {@code
+ *  @Entity
+ *  @Table(name = "example")
+ *  public class ExampleEntity extends BaseEntity {
+ *      @Id
+ *      @Column(name = "id", length = 36)
+ *      private String id;
+ *
+ *      private String name;
+ *      // 생성일, 생성자, 수정일, 수정자, 삭제 정보는 자동 관리
+ *  }
+ * }
+ * 
+ */ +@Getter +@MappedSuperclass +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseEntity { + + /** + * 생성 시간 (서버 시간 기준) + */ + @CreatedDate + @Column(name = "created_at", updatable = false, nullable = false) + private LocalDateTime createdAt; + + /** + * 생성자 ID + */ + @CreatedBy + @Column(name = "created_by", updatable = false, length = 36) + private String createdBy; + + /** + * 수정 시간 (서버 시간 기준) + */ + @LastModifiedDate + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + /** + * 수정자 ID + */ + @LastModifiedBy + @Column(name = "updated_by", length = 36) + private String updatedBy; + + /** + * 삭제 시간 + */ + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + + /** + * 삭제자 ID + */ + @Column(name = "deleted_by", length = 36) + private String deletedBy; + + /** + * 삭제 여부 (Soft Delete) + */ + @Column(name = "is_deleted", nullable = false) + private boolean isDeleted = false; + + /** + * Soft Delete 처리 + * @param deletedBy 삭제자 ID + */ + public void delete(String deletedBy) { + this.isDeleted = true; + this.deletedAt = LocalDateTime.now(); + this.deletedBy = deletedBy; + } + + /** + * Soft Delete 복구 + */ + public void restore() { + this.isDeleted = false; + this.deletedAt = null; + this.deletedBy = null; + } +} \ No newline at end of file diff --git a/src/main/java/com/early_express/slack_service/global/presentation/SwaggerRedirectController.java b/src/main/java/com/early_express/slack_service/global/presentation/SwaggerRedirectController.java new file mode 100644 index 0000000..2f02e19 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/global/presentation/SwaggerRedirectController.java @@ -0,0 +1,21 @@ +package com.early_express.slack_service.global.presentation; + +import io.swagger.v3.oas.annotations.Hidden; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +/** + * 루트 경로를 Swagger UI로 리다이렉트하는 컨트롤러 + */ +@Controller +@Hidden // Swagger 문서에서 숨김 +public class SwaggerRedirectController { + + /** + * 루트 경로(/) 접근 시 Swagger UI로 리다이렉트 + */ + @GetMapping("/") + public String redirectToSwagger() { + return "redirect:/swagger-ui/index.html"; + } +} \ No newline at end of file diff --git a/src/main/java/com/early_express/slack_service/global/presentation/TestController.java b/src/main/java/com/early_express/slack_service/global/presentation/TestController.java new file mode 100644 index 0000000..9503e8a --- /dev/null +++ b/src/main/java/com/early_express/slack_service/global/presentation/TestController.java @@ -0,0 +1,20 @@ +package com.early_express.slack_service.global.presentation; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping +public class TestController { + + @GetMapping("/health") + public String health() { + return "OK"; + } + + @GetMapping("/test") + public String test() { + return "Default Server is working!"; + } +} diff --git a/src/main/java/com/early_express/slack_service/global/presentation/dto/ApiResponse.java b/src/main/java/com/early_express/slack_service/global/presentation/dto/ApiResponse.java new file mode 100644 index 0000000..2f50364 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/global/presentation/dto/ApiResponse.java @@ -0,0 +1,86 @@ +package com.early_express.slack_service.global.presentation.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ApiResponse { + private boolean success; + private String message; + private T data; + private String errorCode; + private Long timestamp; + + /** + * 데이터와 함께 성공 응답 생성 + * */ + public static ApiResponse success(T data) { + return ApiResponse.builder() + .success(true) + .data(data) + .timestamp(System.currentTimeMillis()) + .build(); + } + + /** + * 데이터와 메시지를 포함한 성공 응답 생성 + * */ + public static ApiResponse success(T data, String message) { + return ApiResponse.builder() + .success(true) + .message(message) + .data(data) + .timestamp(System.currentTimeMillis()) + .build(); + } + + /** + * 데이터 없이 성공 응답 생성 + * */ + public static ApiResponse success() { + return ApiResponse.builder() + .success(true) + .timestamp(System.currentTimeMillis()) + .build(); + } + + /** + * 메시지와 함께 실패 응답 생성 + * */ + public static ApiResponse fail(String message) { + return ApiResponse.builder() + .success(false) + .message(message) + .timestamp(System.currentTimeMillis()) + .build(); + } + + /** + * 에러 코드와 함께 실패 응답 생성 + * */ + public static ApiResponse error(String errorCode, String message) { + return ApiResponse.builder() + .success(false) + .errorCode(errorCode) + .message(message) + .timestamp(System.currentTimeMillis()) + .build(); + } + + /** + * 에러 코드, 메시지, 데이터와 함께 실패 응답 생성 + * */ + public static ApiResponse error(String errorCode, String message, T data) { + return ApiResponse.builder() + .success(false) + .errorCode(errorCode) + .message(message) + .data(data) + .timestamp(System.currentTimeMillis()) + .build(); + } +} diff --git a/src/main/java/com/early_express/slack_service/global/presentation/dto/ErrorResponse.java b/src/main/java/com/early_express/slack_service/global/presentation/dto/ErrorResponse.java new file mode 100644 index 0000000..8aab3dd --- /dev/null +++ b/src/main/java/com/early_express/slack_service/global/presentation/dto/ErrorResponse.java @@ -0,0 +1,61 @@ +package com.early_express.slack_service.global.presentation.dto; + +import com.early_express.slack_service.global.presentation.exception.ErrorCode; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +/** + * 에러 응답 DTO + * */ +@Getter +@Builder(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ErrorResponse { + + private final String code; + private final String message; + private final int status; + private final String path; + private final Long timestamp; + + /** + * ErrorCode로 ErrorResponse 생성 + * */ + public static ErrorResponse of(ErrorCode errorCode, String path) { + return ErrorResponse.builder() + .code(errorCode.getCode()) + .message(errorCode.getMessage()) + .status(errorCode.getStatus()) + .path(path) + .timestamp(System.currentTimeMillis()) + .build(); + } + + /** + * ErrorCode와 커스텀 메시지로 ErrorResponse 생성 + * */ + public static ErrorResponse of(ErrorCode errorCode, String customMessage, String path) { + return ErrorResponse.builder() + .code(errorCode.getCode()) + .message(customMessage) + .status(errorCode.getStatus()) + .path(path) + .timestamp(System.currentTimeMillis()) + .build(); + } + + /** + * 직접 파라미터로 ErrorResponse 생성 + * */ + public static ErrorResponse of(String code, String message, int status, String path) { + return ErrorResponse.builder() + .code(code) + .message(message) + .status(status) + .path(path) + .timestamp(System.currentTimeMillis()) + .build(); + } +} diff --git a/src/main/java/com/early_express/slack_service/global/presentation/dto/PageResponse.java b/src/main/java/com/early_express/slack_service/global/presentation/dto/PageResponse.java new file mode 100644 index 0000000..6e36a93 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/global/presentation/dto/PageResponse.java @@ -0,0 +1,33 @@ +package com.early_express.slack_service.global.presentation.dto; + +import com.early_express.slack_service.global.common.dto.PageInfo; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +import java.util.Collections; +import java.util.List; + +@Getter +public class PageResponse { + private final List content; + private final PageInfo pageInfo; + + @JsonCreator + private PageResponse( + @JsonProperty("content") List content, @JsonProperty("pageInfo") PageInfo pageInfo) { + validatePageInfo(pageInfo); + this.content = content != null ? content : Collections.emptyList(); + this.pageInfo = pageInfo; + } + + public static PageResponse of(List content, PageInfo pageInfo) { + return new PageResponse<>(content, pageInfo); + } + + private void validatePageInfo(PageInfo pageInfo) { + if (pageInfo == null) { + throw new IllegalArgumentException("페이지 정보는 null이 될 수 없습니다."); + } + } +} diff --git a/src/main/java/com/early_express/slack_service/global/presentation/exception/AuthErrorCode.java b/src/main/java/com/early_express/slack_service/global/presentation/exception/AuthErrorCode.java new file mode 100644 index 0000000..4771420 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/global/presentation/exception/AuthErrorCode.java @@ -0,0 +1,20 @@ +package com.early_express.slack_service.global.presentation.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum AuthErrorCode implements ErrorCode { + + ACCOUNT_LOCKED("AUTH_001", "계정이 잠겨있습니다.", 423), + ACCOUNT_DISABLED("AUTH_002", "비활성화된 계정입니다. 이메일로 본인 확인 부탁드립니다.", 403), + ACCOUNT_EXPIRED("AUTH_003", "만료된 계정입니다.", 403), + CREDENTIALS_EXPIRED("AUTH_004", "비밀번호가 만료되었습니다.", 403), + INVALID_CREDENTIALS("AUTH_005", "이메일 또는 비밀번호가 올바르지 않습니다.", 401), + AUTHENTICATION_FAILED("AUTH_006", "인증에 실패했습니다.", 401); + + private final String code; + private final String message; + private final int status; +} diff --git a/src/main/java/com/early_express/slack_service/global/presentation/exception/ErrorCode.java b/src/main/java/com/early_express/slack_service/global/presentation/exception/ErrorCode.java new file mode 100644 index 0000000..c4c3c42 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/global/presentation/exception/ErrorCode.java @@ -0,0 +1,23 @@ +package com.early_express.slack_service.global.presentation.exception; + +/** + * 모든 에러 코드 Enum이 구현해야 하는 인터페이스 + * */ +public interface ErrorCode { + + /** + * 에러 코드 반환 (예: "GLOBAL_001") + * */ + String getCode(); + + /** + * 에러 메시지 반환 + * */ + String getMessage(); + + /** + * HTTP 상태 코드 반환 (예: 400, 500) + * */ + int getStatus(); + +} diff --git a/src/main/java/com/early_express/slack_service/global/presentation/exception/GlobalErrorCode.java b/src/main/java/com/early_express/slack_service/global/presentation/exception/GlobalErrorCode.java new file mode 100644 index 0000000..679f883 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/global/presentation/exception/GlobalErrorCode.java @@ -0,0 +1,54 @@ +package com.early_express.slack_service.global.presentation.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 전역 공통 에러 코드 + * */ +@Getter +@RequiredArgsConstructor +public enum GlobalErrorCode implements ErrorCode { + + /** + * 서버 에러 (500번대) + * */ + INTERNAL_SERVER_ERROR("GLOBAL_001", "서버 내부 오류가 발생했습니다.", 500), + SERVICE_UNAVAILABLE("GLOBAL_002", "서비스를 일시적으로 사용할 수 없습니다.", 503), + INVALID_SECURITY_CONTEXT("GLOBAL_003", "보안 컨텍스트에 인증 정보가 존재하지 않습니다.", 500), + + /** + * 클라이언트 에러(400번대) + * */ + BAD_REQUEST("GLOBAL_100", "잘못된 요청입니다.", 400), + INVALID_INPUT_VALUE("GLOBAL_101", "유효하지 않은 입력값입니다.", 400), + INVALID_TYPE_VALUE("GLOBAL_102", "잘못된 데이터 타입입니다.", 400), + MISSING_PARAMETER("GLOBAL_103", "필수 파라미터가 누락되었습니다.", 400), + + /** + * 인증/인가 에러(401, 403) + * */ + UNAUTHORIZED("GLOBAL_200", "인증이 필요합니다.", 401), + INVALID_TOKEN("GLOBAL_201", "유효하지 않은 토큰입니다.", 401), + EXPIRED_TOKEN("GLOBAL_202", "만료된 토큰입니다.", 401), + TOKEN_MISSING("GLOBAL_203", "토큰이 누락되었습니다.", 401), + FORBIDDEN("GLOBAL_210", "접근 권한이 없습니다.", 403), + + /** + * 리소스 관련 에러 (404, 405, 409) + * */ + RESOURCE_NOT_FOUND("GLOBAL_300", "요청한 리소스를 찾을 수 없습니다.", 404), + METHOD_NOT_ALLOWED("GLOBAL_301", "지원하지 않는 HTTP 메소드입니다.", 405), + CONFLICT("GLOBAL_302", "리소스 충돌이 발생했습니다.", 409), + DUPLICATE_RESOURCE("GLOBAL_303", "이미 존재하는 리소스입니다.", 409), + + /** + * 외부 API 에러(502, 504) + * */ + EXTERNAL_API_ERROR("GLOBAL_400", "외부 서비스 연동 중 오류가 발생했습니다.", 502), + EXTERNAL_API_TIMEOUT("GLOBAL_401", "외부 서비스 응답 시간이 초과되었습니다.", 504); + + private final String code; + private final String message; + private final int status; +} diff --git a/src/main/java/com/early_express/slack_service/global/presentation/exception/GlobalException.java b/src/main/java/com/early_express/slack_service/global/presentation/exception/GlobalException.java new file mode 100644 index 0000000..5a65f47 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/global/presentation/exception/GlobalException.java @@ -0,0 +1,61 @@ +package com.early_express.slack_service.global.presentation.exception; + +import lombok.Getter; + +/** + * 모든 커스텀 예외의 기본이 되는 추상 클래스 + * ErrorCode를 포함하여 일관된 예외 처리를 가능하게 함. + * */ +@Getter +public abstract class GlobalException extends RuntimeException { + + private final ErrorCode errorCode; + + /** + * ErrorCode만으로 예외 생성 + * 기본 에러 메시지는 ErrorCode에서 제공하는 메시지를 사용 + * + * @param errorCode 에러 코드 enum 값 + * */ + protected GlobalException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + /** + * ErrorCode와 사용자 정의 메시지로 예외 발생 + * ErrorCode의 메시지 대신 사용자 정의 메시지를 사용 + * + * @param errorCode 에러 코드 enum 값 + * @param message 사용자 정의 에러 메시지 + * */ + protected GlobalException(ErrorCode errorCode, String message) { + super(message); + this.errorCode = errorCode; + } + + /** + * ErrorCode와 원인 예외로 예외 생성 + * 다른 예외를 래핑할 때 사용 + * + * @param errorCode 에러 코드 enum값 + * @param cause 원인이 되는 예외 + * */ + protected GlobalException(ErrorCode errorCode, Throwable cause) { + super(errorCode.getMessage(), cause); + this.errorCode = errorCode; + } + + /** + * ErrorCode, 사용자 정의 메시지, 원인 예외로 예외 생성 + * 사용자 정의 메시지와 원인 예외를 모두 포함할 때 사용 + * + * @param errorCode 에러 코드 enum 값 + * @param message 사용자 정의 에러 메시지 + * @param cause 원인이 되는 예외 + * */ + protected GlobalException(ErrorCode errorCode, String message, Throwable cause) { + super(message, cause); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/com/early_express/slack_service/global/presentation/exception/GlobalExceptionHandler.java b/src/main/java/com/early_express/slack_service/global/presentation/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..78c9d73 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/global/presentation/exception/GlobalExceptionHandler.java @@ -0,0 +1,162 @@ +package com.early_express.slack_service.global.presentation.exception; + +import com.early_express.slack_service.global.presentation.dto.ErrorResponse; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.*; +import org.springframework.security.core.AuthenticationException; +import org.springframework.validation.BindException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 전역 예외 처리기 + * GlobalException과 공통 Spring 예외만 처리 + * */ +@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + /** + * GlobalException 및 모든 하위 예외 통합 처리 + * */ + @ExceptionHandler(GlobalException.class) + public ResponseEntity handleGlobalException( + HttpServletRequest request, + GlobalException e) { + ErrorCode errorCode = e.getErrorCode(); + log.error("글로벌 예외 발생: {} - {}", errorCode.getCode(), e.getMessage(), e); + + ErrorResponse errorResponse = ErrorResponse.of( + errorCode.getCode(), + e.getMessage(), + errorCode.getStatus(), + request.getRequestURI() + ); + + return ResponseEntity + .status(errorCode.getStatus()) + .body(errorResponse); + } + + /** + * 입력값 검증 예외 처리 (@Valid 관련) + * */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException( + HttpServletRequest request, + MethodArgumentNotValidException e) { + + log.error("입력값 검증 실패: {}", e.getMessage()); + + String message = e.getBindingResult() + .getAllErrors() + .getFirst() + .getDefaultMessage(); + + ErrorResponse errorResponse = ErrorResponse.of( + GlobalErrorCode.INVALID_INPUT_VALUE.getCode(), + message != null ? message : GlobalErrorCode.INVALID_INPUT_VALUE.getMessage(), + HttpStatus.BAD_REQUEST.value(), + request.getRequestURI() + ); + + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(errorResponse); + } + + /** + * 바인딩 예외 처리 + * */ + @ExceptionHandler(BindException.class) + public ResponseEntity handleBindException( + HttpServletRequest request, + BindException e) { + + log.error("바인딩 실패: {}", e.getMessage()); + + String message = e.getBindingResult() + .getAllErrors() + .getFirst() + .getDefaultMessage(); + + ErrorResponse errorResponse = ErrorResponse.of( + GlobalErrorCode.INVALID_INPUT_VALUE.getCode(), + message != null ? message : GlobalErrorCode.INVALID_INPUT_VALUE.getMessage(), + HttpStatus.BAD_REQUEST.value(), + request.getRequestURI() + ); + + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(errorResponse); + } + + /** + * 스프링 시큐리티 예외처리 + * */ + @ExceptionHandler({ + LockedException.class, + DisabledException.class, + AccountExpiredException.class, + CredentialsExpiredException.class, + BadCredentialsException.class + }) + public ResponseEntity handleAuthenticationException(HttpServletRequest request, AuthenticationException e) { + + log.error("인증 예외 발생: {}", e.getMessage()); + + ErrorCode errorCode = determineErrorCode(e); + + ErrorResponse errorResponse = ErrorResponse.of( + errorCode, + request.getRequestURI() + ); + + return ResponseEntity + .status(errorCode.getStatus()) + .body(errorResponse); + } + + private ErrorCode determineErrorCode(AuthenticationException e) { + if (e instanceof LockedException) { + return AuthErrorCode.ACCOUNT_LOCKED; + } else if (e instanceof DisabledException) { + return AuthErrorCode.ACCOUNT_DISABLED; + } else if (e instanceof AccountExpiredException) { + return AuthErrorCode.ACCOUNT_EXPIRED; + } else if (e instanceof CredentialsExpiredException) { + return AuthErrorCode.CREDENTIALS_EXPIRED; + } else if (e instanceof BadCredentialsException) { + return AuthErrorCode.INVALID_CREDENTIALS; + } + return AuthErrorCode.AUTHENTICATION_FAILED; + } + + /** + * 기타 모든 예외 처리 + * */ + @ExceptionHandler(Exception.class) + public ResponseEntity handleException( + HttpServletRequest request, + Exception e) { + + log.error("처리되지 않은 예외 발생: {}", e.getMessage(), e); + + ErrorResponse errorResponse = ErrorResponse.of( + GlobalErrorCode.INTERNAL_SERVER_ERROR.getCode(), + GlobalErrorCode.INTERNAL_SERVER_ERROR.getMessage(), + HttpStatus.INTERNAL_SERVER_ERROR.value(), + request.getRequestURI() + ); + + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(errorResponse); + + } +} diff --git a/src/main/java/com/early_express/slack_service/slack/domain/MessageSend.java b/src/main/java/com/early_express/slack_service/slack/domain/MessageSend.java new file mode 100644 index 0000000..44c4bba --- /dev/null +++ b/src/main/java/com/early_express/slack_service/slack/domain/MessageSend.java @@ -0,0 +1,7 @@ +package com.early_express.slack_service.slack.domain; + +import java.util.List; + +public interface MessageSend { + boolean send(List ids, String message); +} diff --git a/src/main/java/com/early_express/slack_service/slack/domain/entity/MessageType.java b/src/main/java/com/early_express/slack_service/slack/domain/entity/MessageType.java new file mode 100644 index 0000000..0bb6189 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/slack/domain/entity/MessageType.java @@ -0,0 +1,6 @@ +package com.early_express.slack_service.slack.domain.entity; + +public enum MessageType { + MORNING_DELIVERY, + HUB_ORDER +} diff --git a/src/main/java/com/early_express/slack_service/slack/domain/entity/Slack.java b/src/main/java/com/early_express/slack_service/slack/domain/entity/Slack.java new file mode 100644 index 0000000..dfaf0f9 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/slack/domain/entity/Slack.java @@ -0,0 +1,37 @@ +package com.early_express.slack_service.slack.domain.entity; + +import com.early_express.slack_service.global.infrastructure.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Table(name = "slack") +@Getter +@ToString +@Entity +@Access(AccessType.FIELD) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Slack extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + @Column(name="slack_id", length=20) + private UUID slackId; + + private String receiverSlackId; + + private String message; + + private LocalDateTime sendAt; + + @Enumerated(EnumType.STRING) + private SlackStatus status; + + @Enumerated(EnumType.STRING) + private MessageType type; + +} diff --git a/src/main/java/com/early_express/slack_service/slack/domain/entity/SlackStatus.java b/src/main/java/com/early_express/slack_service/slack/domain/entity/SlackStatus.java new file mode 100644 index 0000000..394227a --- /dev/null +++ b/src/main/java/com/early_express/slack_service/slack/domain/entity/SlackStatus.java @@ -0,0 +1,7 @@ +package com.early_express.slack_service.slack.domain.entity; + +public enum SlackStatus { + SCHEDULED, + SENT, + FAILED, +} diff --git a/src/main/java/com/early_express/slack_service/slack/infrastructure/slack/SlackMessageSend.java b/src/main/java/com/early_express/slack_service/slack/infrastructure/slack/SlackMessageSend.java new file mode 100644 index 0000000..36958cc --- /dev/null +++ b/src/main/java/com/early_express/slack_service/slack/infrastructure/slack/SlackMessageSend.java @@ -0,0 +1,68 @@ +package com.early_express.slack_service.slack.infrastructure.slack; + +import com.early_express.slack_service.slack.domain.MessageSend; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; + +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +@RefreshScope +public class SlackMessageSend implements MessageSend { + @Value("${SLACK_TOKEN}") + private String token; + + @Override + public boolean send(List ids, String message) { + + if (ids != null) { + + RestClient client = RestClient.builder() + .baseUrl("https://slack.com/api") + .build(); + + + + // Channel ID 처리 S + ResponseEntity response = client.post() + .uri("/conversations.open") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .body(Map.of("users", String.join(",", ids))) + .retrieve() + .toEntity(JsonNode.class); + + System.out.println(response.getBody() + " 응답"); + JsonNode node = response.getBody(); + + if (!response.getStatusCode().is2xxSuccessful() || node.get("ok") == null || !node.get("ok").toString().equals("true")) return false; + + String channelId = node.get("channel").get("id").textValue(); + // Channel ID 처리 E + + // 메세지 발송 처리 S + response = client.post() + .uri("/chat.postMessage") + .header("Authorization", "Bearer " + token) + .body(Map.of("channel", channelId, "text", message, "as_user", true)) + .contentType(MediaType.APPLICATION_JSON) + .retrieve() + .toEntity(JsonNode.class); + + return response.getStatusCode().is2xxSuccessful() && node.get("ok") != null && node.get("ok").toString().equals("true"); + // 메세지 발송 처리 E + } + + + return false; + } +} + diff --git a/src/test/java/com/early_express/slack_service/SlackServiceApplicationTests.java b/src/test/java/com/early_express/slack_service/SlackServiceApplicationTests.java new file mode 100644 index 0000000..b683f56 --- /dev/null +++ b/src/test/java/com/early_express/slack_service/SlackServiceApplicationTests.java @@ -0,0 +1,27 @@ +package com.early_express.slack_service; + +import com.early_express.slack_service.slack.domain.MessageSend; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import java.util.List; + +@SpringBootTest +@ActiveProfiles("test") +class SlackServiceApplicationTests { + + @Test + void contextLoads() { + } + + @Autowired + MessageSend messageSend; + + @Test + void messageSendTest() { + messageSend.send(List.of("U09V1GT3BH8"),"테스트 메세지, 잘 전송이 되나요?"); + } + +} diff --git a/src/test/java/com/earlyexpress/hubRoute_service/HubRouteServiceApplicationTests.java b/src/test/java/com/earlyexpress/hubRoute_service/HubRouteServiceApplicationTests.java deleted file mode 100644 index 8513787..0000000 --- a/src/test/java/com/earlyexpress/hubRoute_service/HubRouteServiceApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.earlyexpress.hubRoute_service; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class HubRouteServiceApplicationTests { - - @Test - void contextLoads() { - } - -} From 957fca096b027de126db452cb6df66a289351e71 Mon Sep 17 00:00:00 2001 From: vsuminv Date: Tue, 25 Nov 2025 18:29:17 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat(slack)=20:=20=EC=8A=A4=EC=BC=80?= =?UTF-8?q?=EC=A5=B4=EB=A7=81=20=EC=A0=81=EC=9A=A9=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EC=8A=AC=EB=9E=99=20=EC=95=8C=EB=A6=BC=20=EB=B0=9C=EC=86=A1=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/exception/SlackErrorCode.java | 20 +++ .../application/exception/SlackException.java | 73 +++++++++ .../internal/SlackSendService.java | 150 ++++++++++++++++++ .../slack/domain/MessageSend.java | 1 + .../slack/domain/entity/Slack.java | 47 ++++-- .../slack/domain/entity/SlackId.java | 31 ++++ .../domain/repository/SlackRepository.java | 14 ++ .../{slack => api}/SlackMessageSend.java | 49 ++++-- .../client/dto/request/SendRequest.java | 15 ++ .../client/dto/response/SendResponse.java | 26 +++ .../config/SchedulerConfig.java | 19 +++ .../internal/SlackSendController.java | 28 ++++ 12 files changed, 446 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/early_express/slack_service/slack/application/exception/SlackErrorCode.java create mode 100644 src/main/java/com/early_express/slack_service/slack/application/exception/SlackException.java create mode 100644 src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java create mode 100644 src/main/java/com/early_express/slack_service/slack/domain/entity/SlackId.java create mode 100644 src/main/java/com/early_express/slack_service/slack/domain/repository/SlackRepository.java rename src/main/java/com/early_express/slack_service/slack/infrastructure/{slack => api}/SlackMessageSend.java (52%) create mode 100644 src/main/java/com/early_express/slack_service/slack/infrastructure/client/dto/request/SendRequest.java create mode 100644 src/main/java/com/early_express/slack_service/slack/infrastructure/client/dto/response/SendResponse.java create mode 100644 src/main/java/com/early_express/slack_service/slack/infrastructure/config/SchedulerConfig.java create mode 100644 src/main/java/com/early_express/slack_service/slack/infrastructure/presentation/internal/SlackSendController.java diff --git a/src/main/java/com/early_express/slack_service/slack/application/exception/SlackErrorCode.java b/src/main/java/com/early_express/slack_service/slack/application/exception/SlackErrorCode.java new file mode 100644 index 0000000..97ab4b4 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/slack/application/exception/SlackErrorCode.java @@ -0,0 +1,20 @@ +package com.early_express.slack_service.slack.application.exception; + +import com.early_express.slack_service.global.presentation.exception.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + + +@Getter +@RequiredArgsConstructor +public enum SlackErrorCode implements ErrorCode { + SLACK_NOT_FOUND("SLACK_001","슬랙 아이디를 찾을 수 없습니다.",404), + SLACK_RETRY_FAILED("SLACK", "재시도 횟수 초가로 전송 실패하였습니다.",400), + SLACK_DB_SAVE_FAILED("SLACK", "DB저장에 실패하였습니다.",400), + SLACK_SEND_FAILED("SLACK","메세지 전송에 실패하였습니다.",400), + SLACK_SCHEDULE_MISS_TIME("SLACK", "요청하신 시간이 아닙니다.",400), + SLACK_CHANEL_CREATE_FAILED("SLACK","채널 생성 실패하였습니다.",400); + private final String code; + private final String message; + private final int status; +} diff --git a/src/main/java/com/early_express/slack_service/slack/application/exception/SlackException.java b/src/main/java/com/early_express/slack_service/slack/application/exception/SlackException.java new file mode 100644 index 0000000..a601889 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/slack/application/exception/SlackException.java @@ -0,0 +1,73 @@ +package com.early_express.slack_service.slack.application.exception; + + +import com.early_express.slack_service.global.presentation.exception.ErrorCode; +import com.early_express.slack_service.global.presentation.exception.GlobalException; + +/** + * Hub 도메인 전용 예외 + * GlobalException을 상속하여 일관된 예외 처리 + */ +public class SlackException extends GlobalException { + + /** + * ErrorCode만으로 예외 생성 + * + * @param errorCode Inventory 에러 코드 + */ + public SlackException(SlackErrorCode errorCode) { + super(errorCode); + } + + /** + * ErrorCode와 커스텀 메시지로 예외 생성 + * + * @param errorCode Inventory 에러 코드 + * @param message 커스텀 에러 메시지 + */ + public SlackException(SlackErrorCode errorCode, String message) { + super(errorCode, message); + } + + /** + * ErrorCode와 원인 예외로 예외 생성 + * + * @param errorCode Inventory 에러 코드 + * @param cause 원인 예외 + */ + public SlackException(SlackErrorCode errorCode, Throwable cause) { + super(errorCode, cause); + } + + /** + * ErrorCode, 커스텀 메시지, 원인 예외로 예외 생성 + * + * @param errorCode Inventory 에러 코드 + * @param message 커스텀 에러 메시지 + * @param cause 원인 예외 + */ + public SlackException(SlackErrorCode errorCode, String message, Throwable cause) { + super(errorCode, message, cause); + } + + /** + * ErrorCode를 받아서 예외 생성 (다른 ErrorCode도 사용 가능) + * 외부 서비스 에러 등을 래핑할 때 사용 + * + * @param errorCode 임의의 ErrorCode + */ + public SlackException(ErrorCode errorCode) { + super(errorCode); + } + + /** + * ErrorCode와 커스텀 메시지로 예외 생성 (다른 ErrorCode도 사용 가능) + * + * @param errorCode 임의의 ErrorCode + * @param message 커스텀 에러 메시지 + */ + public SlackException(ErrorCode errorCode, String message) { + super(errorCode, message); + } +} + diff --git a/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java b/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java new file mode 100644 index 0000000..f199f20 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java @@ -0,0 +1,150 @@ +package com.early_express.slack_service.slack.application.internal; + +import com.early_express.slack_service.slack.application.exception.SlackException; +import com.early_express.slack_service.slack.domain.MessageSend; +import com.early_express.slack_service.slack.domain.entity.MessageType; +import com.early_express.slack_service.slack.domain.entity.Slack; +import com.early_express.slack_service.slack.domain.entity.SlackId; +import com.early_express.slack_service.slack.domain.entity.SlackStatus; +import com.early_express.slack_service.slack.domain.repository.SlackRepository; +import com.early_express.slack_service.slack.infrastructure.client.dto.request.SendRequest; +import com.early_express.slack_service.slack.infrastructure.client.dto.response.SendResponse; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.TaskScheduler; + +import org.springframework.scheduling.support.CronTrigger; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +import static com.early_express.slack_service.global.presentation.exception.GlobalErrorCode.MISSING_PARAMETER; +import static com.early_express.slack_service.slack.application.exception.SlackErrorCode.*; + +@Service +@RequiredArgsConstructor +@Transactional +public class SlackSendService { + private final SlackRepository slackRepository; + private final MessageSend messageSend; + private final TaskScheduler taskScheduler; + + private final int SCHEDULE_HOUR = 18; + private final int SCHEDULE_MINUTE = 0; + + + +// public SendResponse sendDeliveryMessage(SendRequest sendRequest) throws Exception { +// if(sendRequest.getReceiverId() == null || sendRequest.getReceiverId().isEmpty()){ +// +// throw new SlackException(MISSING_PARAMETER); +// } +// +// +// +// boolean call_result = callSlack(sendRequest); +// SlackStatus status = call_result ? SlackStatus.SENT : SlackStatus.FAILED; +// String errorMessage = call_result ? null : SLACK_SEND_FAILED.getMessage(); +// LocalDateTime sentAt = call_result ?LocalDateTime.now(): null ; +// +// +// Slack slack = Slack.builder() +// .slackId(SlackId.of()) +// .receiverSlackId(sendRequest.getReceiverId()) +// .message(sendRequest.getMessage()) +// .status(status) +// .type(MessageType.MORNING_DELIVERY) +// .sentAt(sentAt) +// .errorMessage(errorMessage) +// .build(); +// try{ +// slackRepository.save(slack); +// }catch(Exception e){ +// throw new SlackException(SLACK_DB_SAVE_FAILED); +// } +// +// return new SendResponse( +// slack.getSlackId().getId(), +// slack.getReceiverSlackId(), +// slack.getStatus(), +// slack.getSentAt(), +// slack.getErrorMessage() +// ); +// } + + public void sendDeliveryMessage(SendRequest sendRequest) throws Exception { + if (sendRequest.getReceiverId() == null || sendRequest.getReceiverId().isEmpty()) { + throw new SlackException(MISSING_PARAMETER); + } + + + boolean call_result = callSlack(sendRequest); + SlackStatus status = call_result ? SlackStatus.SENT : SlackStatus.FAILED; + String errorMessage = call_result ? null : SLACK_SEND_FAILED.getMessage(); + LocalDateTime sentAt = call_result ?LocalDateTime.now(): null ; + + + // DB 저장 + Slack slack = Slack.builder() + .slackId(SlackId.of()) + .receiverSlackId(sendRequest.getReceiverId()) + .message(sendRequest.getMessage()) + .status(status) + .type(MessageType.MORNING_DELIVERY) + .sentAt(sentAt) + .errorMessage(errorMessage) + .build(); + + try { + slackRepository.save(slack); + } catch(Exception e){ + e.printStackTrace(); + throw new SlackException(SLACK_DB_SAVE_FAILED); + + } + } + + + // api 호출 + public boolean callSlack( SendRequest sendRequest) throws Exception { + if(messageSend == null) return false; + List id =List.of(sendRequest.getReceiverId()); + boolean result = messageSend.send(id, sendRequest.getMessage()); + + return result; + + } + + // 스케줄링 + public SendResponse schedule_delivery(SendRequest sendRequest) throws Exception{ + + + Runnable schedule_delivery = new Runnable() + { + @Override + public void run() { + try { + sendDeliveryMessage(sendRequest); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + taskScheduler.schedule(schedule_delivery, new CronTrigger("0 50 17 * * *")); + + + + + return new SendResponse( + + SlackId.of().getId(), sendRequest.getReceiverId(),sendRequest.getSlackStatus(),LocalDateTime.now(),""); + } + + + + + +} + diff --git a/src/main/java/com/early_express/slack_service/slack/domain/MessageSend.java b/src/main/java/com/early_express/slack_service/slack/domain/MessageSend.java index 44c4bba..c181c76 100644 --- a/src/main/java/com/early_express/slack_service/slack/domain/MessageSend.java +++ b/src/main/java/com/early_express/slack_service/slack/domain/MessageSend.java @@ -4,4 +4,5 @@ public interface MessageSend { boolean send(List ids, String message); + } diff --git a/src/main/java/com/early_express/slack_service/slack/domain/entity/Slack.java b/src/main/java/com/early_express/slack_service/slack/domain/entity/Slack.java index dfaf0f9..ac8acdc 100644 --- a/src/main/java/com/early_express/slack_service/slack/domain/entity/Slack.java +++ b/src/main/java/com/early_express/slack_service/slack/domain/entity/Slack.java @@ -1,13 +1,12 @@ package com.early_express.slack_service.slack.domain.entity; import com.early_express.slack_service.global.infrastructure.entity.BaseEntity; +import com.early_express.slack_service.slack.domain.MessageSend; import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.ToString; +import lombok.*; import java.time.LocalDateTime; +import java.util.List; import java.util.UUID; @Table(name = "slack") @@ -16,22 +15,50 @@ @Entity @Access(AccessType.FIELD) @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Builder +@AllArgsConstructor(access = AccessLevel.PROTECTED) public class Slack extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.UUID) - @Column(name="slack_id", length=20) - private UUID slackId; + @EmbeddedId + private SlackId slackId; + + @Column(nullable=false) private String receiverSlackId; - private String message; - private LocalDateTime sendAt; + private String message; + @Column(nullable=false) @Enumerated(EnumType.STRING) private SlackStatus status; + @Column(nullable=false) @Enumerated(EnumType.STRING) private MessageType type; + private LocalDateTime sentAt; + + private String errorMessage; + + + +// @Builder +// public Slack(UUID slackId, String receiverSlackId, String message, SlackStatus status, MessageType type, String errorMessage, LocalDateTime sendAt, MessageSend messageSend) { +// this.slackId = slackId; +// if( receiverSlackId == null || receiverSlackId.isEmpty() ) this.receiverSlackId = UUID.randomUUID().toString(); +// this.message = message; +// this.sendAt = sendAt; +// this.status = status; +// this.type = type; +// this.errorMessage = errorMessage; +// this.sendAt = LocalDateTime.now(); +// setMessage(messageSend); +// } +// +// private void setMessage(MessageSend messageSend) { +// if (messageSend == null) return; +// List ids = List.of(this.receiverSlackId); +// String message = this.message; +// boolean result = messageSend.send(ids,message); +// } } diff --git a/src/main/java/com/early_express/slack_service/slack/domain/entity/SlackId.java b/src/main/java/com/early_express/slack_service/slack/domain/entity/SlackId.java new file mode 100644 index 0000000..556b108 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/slack/domain/entity/SlackId.java @@ -0,0 +1,31 @@ +package com.early_express.slack_service.slack.domain.entity; + +import com.fasterxml.jackson.annotation.JacksonInject; +import jakarta.persistence.Embeddable; +import jakarta.persistence.NamedEntityGraph; +import lombok.*; + +import java.util.UUID; + +@ToString +@Getter +@Embeddable +@EqualsAndHashCode +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class SlackId { + private UUID id; + + protected SlackId(UUID id) { + this.id = id; + } + + + public static SlackId of(UUID id) { + return new SlackId(id); + } + + public static SlackId of() { + return SlackId.of(UUID.randomUUID()); + } + +} diff --git a/src/main/java/com/early_express/slack_service/slack/domain/repository/SlackRepository.java b/src/main/java/com/early_express/slack_service/slack/domain/repository/SlackRepository.java new file mode 100644 index 0000000..862d329 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/slack/domain/repository/SlackRepository.java @@ -0,0 +1,14 @@ +package com.early_express.slack_service.slack.domain.repository; + +import com.early_express.slack_service.slack.domain.entity.Slack; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface SlackRepository extends JpaRepository { + +} + diff --git a/src/main/java/com/early_express/slack_service/slack/infrastructure/slack/SlackMessageSend.java b/src/main/java/com/early_express/slack_service/slack/infrastructure/api/SlackMessageSend.java similarity index 52% rename from src/main/java/com/early_express/slack_service/slack/infrastructure/slack/SlackMessageSend.java rename to src/main/java/com/early_express/slack_service/slack/infrastructure/api/SlackMessageSend.java index 36958cc..ff1fb72 100644 --- a/src/main/java/com/early_express/slack_service/slack/infrastructure/slack/SlackMessageSend.java +++ b/src/main/java/com/early_express/slack_service/slack/infrastructure/api/SlackMessageSend.java @@ -1,5 +1,8 @@ -package com.early_express.slack_service.slack.infrastructure.slack; +package com.early_express.slack_service.slack.infrastructure.api; +import com.early_express.slack_service.global.presentation.exception.GlobalException; +import com.early_express.slack_service.slack.application.exception.SlackErrorCode; +import com.early_express.slack_service.slack.application.exception.SlackException; import com.early_express.slack_service.slack.domain.MessageSend; import com.fasterxml.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; @@ -13,6 +16,9 @@ import java.util.List; import java.util.Map; +import static com.early_express.slack_service.slack.application.exception.SlackErrorCode.SLACK_CHANEL_CREATE_FAILED; +import static com.early_express.slack_service.slack.application.exception.SlackErrorCode.SLACK_SEND_FAILED; + @Slf4j @Service @RefreshScope @@ -22,16 +28,16 @@ public class SlackMessageSend implements MessageSend { @Override public boolean send(List ids, String message) { - - if (ids != null) { + try { + if (ids == null || ids.isEmpty()) { + throw new IllegalArgumentException("Slack ID가 없습니다."); + } RestClient client = RestClient.builder() .baseUrl("https://slack.com/api") .build(); - - - // Channel ID 처리 S + // 채널 생성 ResponseEntity response = client.post() .uri("/conversations.open") .header("Authorization", "Bearer " + token) @@ -40,29 +46,38 @@ public boolean send(List ids, String message) { .retrieve() .toEntity(JsonNode.class); - System.out.println(response.getBody() + " 응답"); JsonNode node = response.getBody(); + if (!response.getStatusCode().is2xxSuccessful() || + node.get("ok") == null || !node.get("ok").asBoolean()) { + throw new SlackException(SLACK_CHANEL_CREATE_FAILED); + } - if (!response.getStatusCode().is2xxSuccessful() || node.get("ok") == null || !node.get("ok").toString().equals("true")) return false; - - String channelId = node.get("channel").get("id").textValue(); - // Channel ID 처리 E + String channelId = node.get("channel").get("id").asText(); - // 메세지 발송 처리 S + // 메시지 발송 response = client.post() .uri("/chat.postMessage") .header("Authorization", "Bearer " + token) - .body(Map.of("channel", channelId, "text", message, "as_user", true)) .contentType(MediaType.APPLICATION_JSON) + .body(Map.of("channel", channelId, "text", message, "as_user", true)) .retrieve() .toEntity(JsonNode.class); - return response.getStatusCode().is2xxSuccessful() && node.get("ok") != null && node.get("ok").toString().equals("true"); - // 메세지 발송 처리 E - } + node = response.getBody(); + if (!response.getStatusCode().is2xxSuccessful() || + node.get("ok") == null || !node.get("ok").asBoolean()) { + throw new SlackException(SLACK_SEND_FAILED); + } + + return true; + } catch (Exception e) { + // 로깅 후 Service로 예외 던짐 + log.error("Slack API 호출 실패", e); + throw new SlackException(SLACK_SEND_FAILED,e.getMessage()); - return false; + } } + } diff --git a/src/main/java/com/early_express/slack_service/slack/infrastructure/client/dto/request/SendRequest.java b/src/main/java/com/early_express/slack_service/slack/infrastructure/client/dto/request/SendRequest.java new file mode 100644 index 0000000..423fb41 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/slack/infrastructure/client/dto/request/SendRequest.java @@ -0,0 +1,15 @@ +package com.early_express.slack_service.slack.infrastructure.client.dto.request; + +import com.early_express.slack_service.slack.domain.entity.MessageType; +import com.early_express.slack_service.slack.domain.entity.SlackStatus; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class SendRequest { + private String receiverId; + private String message; + private MessageType messageType; + private SlackStatus slackStatus; +} diff --git a/src/main/java/com/early_express/slack_service/slack/infrastructure/client/dto/response/SendResponse.java b/src/main/java/com/early_express/slack_service/slack/infrastructure/client/dto/response/SendResponse.java new file mode 100644 index 0000000..3f4e12c --- /dev/null +++ b/src/main/java/com/early_express/slack_service/slack/infrastructure/client/dto/response/SendResponse.java @@ -0,0 +1,26 @@ +package com.early_express.slack_service.slack.infrastructure.client.dto.response; + + +import com.early_express.slack_service.slack.domain.entity.SlackStatus; +import com.early_express.slack_service.slack.infrastructure.client.dto.request.SendRequest; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SendResponse { + private UUID slackId; + private String receiverSlackId; + private SlackStatus status; + private LocalDateTime sentAt; + private String errorMessage; + + +} diff --git a/src/main/java/com/early_express/slack_service/slack/infrastructure/config/SchedulerConfig.java b/src/main/java/com/early_express/slack_service/slack/infrastructure/config/SchedulerConfig.java new file mode 100644 index 0000000..ed14f03 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/slack/infrastructure/config/SchedulerConfig.java @@ -0,0 +1,19 @@ +package com.early_express.slack_service.slack.infrastructure.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +@Configuration +@EnableScheduling +public class SchedulerConfig { + @Bean + public TaskScheduler taskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setPoolSize(5); + scheduler.initialize(); + return scheduler; + } +} diff --git a/src/main/java/com/early_express/slack_service/slack/infrastructure/presentation/internal/SlackSendController.java b/src/main/java/com/early_express/slack_service/slack/infrastructure/presentation/internal/SlackSendController.java new file mode 100644 index 0000000..0984cc5 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/slack/infrastructure/presentation/internal/SlackSendController.java @@ -0,0 +1,28 @@ +package com.early_express.slack_service.slack.infrastructure.presentation.internal; + + +import com.early_express.slack_service.slack.application.internal.SlackSendService; +import com.early_express.slack_service.slack.infrastructure.client.dto.request.SendRequest; +import com.early_express.slack_service.slack.infrastructure.client.dto.response.SendResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/v1/slack/internal") +@RequiredArgsConstructor +public class SlackSendController { + + private final SlackSendService slackSendService; + + @PostMapping("/delivery-sent") + public ResponseEntity sendDeliveryMessage(@RequestBody SendRequest sendRequest) throws Exception { + + SendResponse response =slackSendService.schedule_delivery(sendRequest); + return ResponseEntity.ok(response); + + } +} From b5f3f691025f76fa3fed6ed93da83145655d23c7 Mon Sep 17 00:00:00 2001 From: vsuminv Date: Tue, 25 Nov 2025 18:32:30 +0900 Subject: [PATCH 3/7] =?UTF-8?q?refactor(errorcode):=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=EC=83=81=ED=83=9C=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/exception/SlackErrorCode.java | 10 ++--- .../internal/SlackSendService.java | 42 ------------------- 2 files changed, 5 insertions(+), 47 deletions(-) diff --git a/src/main/java/com/early_express/slack_service/slack/application/exception/SlackErrorCode.java b/src/main/java/com/early_express/slack_service/slack/application/exception/SlackErrorCode.java index 97ab4b4..53fd11c 100644 --- a/src/main/java/com/early_express/slack_service/slack/application/exception/SlackErrorCode.java +++ b/src/main/java/com/early_express/slack_service/slack/application/exception/SlackErrorCode.java @@ -9,11 +9,11 @@ @RequiredArgsConstructor public enum SlackErrorCode implements ErrorCode { SLACK_NOT_FOUND("SLACK_001","슬랙 아이디를 찾을 수 없습니다.",404), - SLACK_RETRY_FAILED("SLACK", "재시도 횟수 초가로 전송 실패하였습니다.",400), - SLACK_DB_SAVE_FAILED("SLACK", "DB저장에 실패하였습니다.",400), - SLACK_SEND_FAILED("SLACK","메세지 전송에 실패하였습니다.",400), - SLACK_SCHEDULE_MISS_TIME("SLACK", "요청하신 시간이 아닙니다.",400), - SLACK_CHANEL_CREATE_FAILED("SLACK","채널 생성 실패하였습니다.",400); + SLACK_DB_SAVE_FAILED("SLACK_002", "DB저장에 실패하였습니다.",500), + SLACK_SEND_FAILED("SLACK_003","메세지 전송에 실패하였습니다.",502), + SLACK_SCHEDULE_MISS_TIME("SLACK_004", "요청하신 시간이 아닙니다.",400), + SLACK_CHANEL_CREATE_FAILED("SLACK_005","채널 생성 실패하였습니다.",500); + private final String code; private final String message; private final int status; diff --git a/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java b/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java index f199f20..ec83573 100644 --- a/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java +++ b/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java @@ -30,48 +30,6 @@ public class SlackSendService { private final MessageSend messageSend; private final TaskScheduler taskScheduler; - private final int SCHEDULE_HOUR = 18; - private final int SCHEDULE_MINUTE = 0; - - - -// public SendResponse sendDeliveryMessage(SendRequest sendRequest) throws Exception { -// if(sendRequest.getReceiverId() == null || sendRequest.getReceiverId().isEmpty()){ -// -// throw new SlackException(MISSING_PARAMETER); -// } -// -// -// -// boolean call_result = callSlack(sendRequest); -// SlackStatus status = call_result ? SlackStatus.SENT : SlackStatus.FAILED; -// String errorMessage = call_result ? null : SLACK_SEND_FAILED.getMessage(); -// LocalDateTime sentAt = call_result ?LocalDateTime.now(): null ; -// -// -// Slack slack = Slack.builder() -// .slackId(SlackId.of()) -// .receiverSlackId(sendRequest.getReceiverId()) -// .message(sendRequest.getMessage()) -// .status(status) -// .type(MessageType.MORNING_DELIVERY) -// .sentAt(sentAt) -// .errorMessage(errorMessage) -// .build(); -// try{ -// slackRepository.save(slack); -// }catch(Exception e){ -// throw new SlackException(SLACK_DB_SAVE_FAILED); -// } -// -// return new SendResponse( -// slack.getSlackId().getId(), -// slack.getReceiverSlackId(), -// slack.getStatus(), -// slack.getSentAt(), -// slack.getErrorMessage() -// ); -// } public void sendDeliveryMessage(SendRequest sendRequest) throws Exception { if (sendRequest.getReceiverId() == null || sendRequest.getReceiverId().isEmpty()) { From e4c8c521b2ac2675e90481a4e726afe2b4f46caa Mon Sep 17 00:00:00 2001 From: vsuminv Date: Wed, 26 Nov 2025 11:58:18 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat(serviec):=20=EC=8A=AC=EB=9E=99=20?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=A0=84=EC=B2=B4=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/SlackSendService.java | 9 +-- .../application/web/SlackSelectService.java | 33 +++++++++++ .../slack/domain/entity/Slack.java | 20 ------- .../slack/domain/entity/SlackStatus.java | 1 - .../domain/repository/SlackRepository.java | 8 ++- .../client/dto/response/SendResponse.java | 6 +- .../config/redis/RedisConfig.java | 54 ++++++++++++++++++ .../{ => schedule}/SchedulerConfig.java | 2 +- .../internal/SlackSendController.java | 2 + .../web/SlackSelectController.java | 45 +++++++++++++++ .../repository/SlackRepositoryCustom.java | 9 +++ .../repository/SlackRepositoryImpl.java | 55 +++++++++++++++++++ 12 files changed, 216 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/early_express/slack_service/slack/application/web/SlackSelectService.java create mode 100644 src/main/java/com/early_express/slack_service/slack/infrastructure/config/redis/RedisConfig.java rename src/main/java/com/early_express/slack_service/slack/infrastructure/config/{ => schedule}/SchedulerConfig.java (97%) create mode 100644 src/main/java/com/early_express/slack_service/slack/infrastructure/presentation/web/SlackSelectController.java create mode 100644 src/main/java/com/early_express/slack_service/slack/infrastructure/repository/SlackRepositoryCustom.java create mode 100644 src/main/java/com/early_express/slack_service/slack/infrastructure/repository/SlackRepositoryImpl.java diff --git a/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java b/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java index ec83573..2f25165 100644 --- a/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java +++ b/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java @@ -11,6 +11,7 @@ import com.early_express.slack_service.slack.infrastructure.client.dto.response.SendResponse; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.CacheEvict; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.support.CronTrigger; @@ -48,6 +49,7 @@ public void sendDeliveryMessage(SendRequest sendRequest) throws Exception { .slackId(SlackId.of()) .receiverSlackId(sendRequest.getReceiverId()) .message(sendRequest.getMessage()) + .type(sendRequest.getMessageType()) .status(status) .type(MessageType.MORNING_DELIVERY) .sentAt(sentAt) @@ -75,6 +77,7 @@ public boolean callSlack( SendRequest sendRequest) throws Exception { } // 스케줄링 + @CacheEvict(value = "slack", allEntries = true) public SendResponse schedule_delivery(SendRequest sendRequest) throws Exception{ @@ -90,19 +93,17 @@ public void run() { } } }; - taskScheduler.schedule(schedule_delivery, new CronTrigger("0 50 17 * * *")); + taskScheduler.schedule(schedule_delivery, new CronTrigger("0 39 11 * * *")); return new SendResponse( - SlackId.of().getId(), sendRequest.getReceiverId(),sendRequest.getSlackStatus(),LocalDateTime.now(),""); + SlackId.of().getId(), sendRequest.getReceiverId(),sendRequest.getMessage(),sendRequest.getMessageType(),sendRequest.getSlackStatus(),LocalDateTime.now(),""); } - - } diff --git a/src/main/java/com/early_express/slack_service/slack/application/web/SlackSelectService.java b/src/main/java/com/early_express/slack_service/slack/application/web/SlackSelectService.java new file mode 100644 index 0000000..0da4ce4 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/slack/application/web/SlackSelectService.java @@ -0,0 +1,33 @@ +package com.early_express.slack_service.slack.application.web; + + +import com.early_express.slack_service.global.common.utils.PageUtils; +import com.early_express.slack_service.global.presentation.dto.PageResponse; +import com.early_express.slack_service.slack.domain.entity.Slack; +import com.early_express.slack_service.slack.domain.repository.SlackRepository; +import com.early_express.slack_service.slack.infrastructure.client.dto.response.SendResponse; +import com.early_express.slack_service.slack.infrastructure.repository.SlackRepositoryImpl; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.cache.annotation.Cacheable; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.util.function.Function; + +@Service +@RequiredArgsConstructor +public class SlackSelectService { + private final SlackRepositoryImpl slackRepositoryImpl; + + @Transactional(readOnly = true) + @Cacheable(value = "slack", key = " #page + ':' + #size + ':' + #sortType") + public PageResponse getSelectAll(int page, int size, String sortType) { + + Page slackPage = slackRepositoryImpl.slackList(page, size, sortType); + + return PageUtils.toPageResponse(slackPage); + } +} diff --git a/src/main/java/com/early_express/slack_service/slack/domain/entity/Slack.java b/src/main/java/com/early_express/slack_service/slack/domain/entity/Slack.java index ac8acdc..0da3c25 100644 --- a/src/main/java/com/early_express/slack_service/slack/domain/entity/Slack.java +++ b/src/main/java/com/early_express/slack_service/slack/domain/entity/Slack.java @@ -41,24 +41,4 @@ public class Slack extends BaseEntity { private String errorMessage; - -// @Builder -// public Slack(UUID slackId, String receiverSlackId, String message, SlackStatus status, MessageType type, String errorMessage, LocalDateTime sendAt, MessageSend messageSend) { -// this.slackId = slackId; -// if( receiverSlackId == null || receiverSlackId.isEmpty() ) this.receiverSlackId = UUID.randomUUID().toString(); -// this.message = message; -// this.sendAt = sendAt; -// this.status = status; -// this.type = type; -// this.errorMessage = errorMessage; -// this.sendAt = LocalDateTime.now(); -// setMessage(messageSend); -// } -// -// private void setMessage(MessageSend messageSend) { -// if (messageSend == null) return; -// List ids = List.of(this.receiverSlackId); -// String message = this.message; -// boolean result = messageSend.send(ids,message); -// } } diff --git a/src/main/java/com/early_express/slack_service/slack/domain/entity/SlackStatus.java b/src/main/java/com/early_express/slack_service/slack/domain/entity/SlackStatus.java index 394227a..c692a15 100644 --- a/src/main/java/com/early_express/slack_service/slack/domain/entity/SlackStatus.java +++ b/src/main/java/com/early_express/slack_service/slack/domain/entity/SlackStatus.java @@ -1,7 +1,6 @@ package com.early_express.slack_service.slack.domain.entity; public enum SlackStatus { - SCHEDULED, SENT, FAILED, } diff --git a/src/main/java/com/early_express/slack_service/slack/domain/repository/SlackRepository.java b/src/main/java/com/early_express/slack_service/slack/domain/repository/SlackRepository.java index 862d329..ccf0e21 100644 --- a/src/main/java/com/early_express/slack_service/slack/domain/repository/SlackRepository.java +++ b/src/main/java/com/early_express/slack_service/slack/domain/repository/SlackRepository.java @@ -1,6 +1,10 @@ package com.early_express.slack_service.slack.domain.repository; import com.early_express.slack_service.slack.domain.entity.Slack; +import com.early_express.slack_service.slack.infrastructure.client.dto.response.SendResponse; +import com.early_express.slack_service.slack.infrastructure.repository.SlackRepositoryCustom; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -8,7 +12,9 @@ import java.util.UUID; @Repository -public interface SlackRepository extends JpaRepository { +public interface SlackRepository extends JpaRepository, SlackRepositoryCustom { + //Page slackList(Pageable pageable); + Page slackList(int page, int size,String sortType); } diff --git a/src/main/java/com/early_express/slack_service/slack/infrastructure/client/dto/response/SendResponse.java b/src/main/java/com/early_express/slack_service/slack/infrastructure/client/dto/response/SendResponse.java index 3f4e12c..d9ae200 100644 --- a/src/main/java/com/early_express/slack_service/slack/infrastructure/client/dto/response/SendResponse.java +++ b/src/main/java/com/early_express/slack_service/slack/infrastructure/client/dto/response/SendResponse.java @@ -1,6 +1,7 @@ package com.early_express.slack_service.slack.infrastructure.client.dto.response; +import com.early_express.slack_service.slack.domain.entity.MessageType; import com.early_express.slack_service.slack.domain.entity.SlackStatus; import com.early_express.slack_service.slack.infrastructure.client.dto.request.SendRequest; import lombok.AllArgsConstructor; @@ -8,6 +9,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.io.Serializable; import java.time.LocalDateTime; import java.util.UUID; @@ -15,9 +17,11 @@ @Builder @AllArgsConstructor @NoArgsConstructor -public class SendResponse { +public class SendResponse { private UUID slackId; private String receiverSlackId; + private String message; + private MessageType messageType; private SlackStatus status; private LocalDateTime sentAt; private String errorMessage; diff --git a/src/main/java/com/early_express/slack_service/slack/infrastructure/config/redis/RedisConfig.java b/src/main/java/com/early_express/slack_service/slack/infrastructure/config/redis/RedisConfig.java new file mode 100644 index 0000000..8d191b0 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/slack/infrastructure/config/redis/RedisConfig.java @@ -0,0 +1,54 @@ +package com.early_express.slack_service.slack.infrastructure.config.redis; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.data.web.config.EnableSpringDataWebSupport; + +import static org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO; + +@Configuration +@RequiredArgsConstructor +@EnableCaching +@EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO) +public class RedisConfig { + private final RedisProperties redisProperties; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort()); + } + + @Bean + public CacheManager cacheManager(RedisConnectionFactory factory) { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); + GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(objectMapper); + + + RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith(RedisSerializationContext + .SerializationPair.fromSerializer(new StringRedisSerializer())) + .serializeValuesWith(RedisSerializationContext + .SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer)); + + return RedisCacheManager + .RedisCacheManagerBuilder + .fromConnectionFactory(factory) + .cacheDefaults(cacheConfig) + .build(); + } +} diff --git a/src/main/java/com/early_express/slack_service/slack/infrastructure/config/SchedulerConfig.java b/src/main/java/com/early_express/slack_service/slack/infrastructure/config/schedule/SchedulerConfig.java similarity index 97% rename from src/main/java/com/early_express/slack_service/slack/infrastructure/config/SchedulerConfig.java rename to src/main/java/com/early_express/slack_service/slack/infrastructure/config/schedule/SchedulerConfig.java index ed14f03..b2762ee 100644 --- a/src/main/java/com/early_express/slack_service/slack/infrastructure/config/SchedulerConfig.java +++ b/src/main/java/com/early_express/slack_service/slack/infrastructure/config/schedule/SchedulerConfig.java @@ -1,4 +1,4 @@ -package com.early_express.slack_service.slack.infrastructure.config; +package com.early_express.slack_service.slack.infrastructure.config.schedule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/com/early_express/slack_service/slack/infrastructure/presentation/internal/SlackSendController.java b/src/main/java/com/early_express/slack_service/slack/infrastructure/presentation/internal/SlackSendController.java index 0984cc5..8e54e75 100644 --- a/src/main/java/com/early_express/slack_service/slack/infrastructure/presentation/internal/SlackSendController.java +++ b/src/main/java/com/early_express/slack_service/slack/infrastructure/presentation/internal/SlackSendController.java @@ -25,4 +25,6 @@ public ResponseEntity sendDeliveryMessage(@RequestBody SendRequest return ResponseEntity.ok(response); } + + } diff --git a/src/main/java/com/early_express/slack_service/slack/infrastructure/presentation/web/SlackSelectController.java b/src/main/java/com/early_express/slack_service/slack/infrastructure/presentation/web/SlackSelectController.java new file mode 100644 index 0000000..0cbf117 --- /dev/null +++ b/src/main/java/com/early_express/slack_service/slack/infrastructure/presentation/web/SlackSelectController.java @@ -0,0 +1,45 @@ +package com.early_express.slack_service.slack.infrastructure.presentation.web; + + +import com.early_express.slack_service.global.common.utils.PageUtils; +import com.early_express.slack_service.global.presentation.dto.PageResponse; +import com.early_express.slack_service.slack.application.web.SlackSelectService; +import com.early_express.slack_service.slack.domain.entity.Slack; +import com.early_express.slack_service.slack.infrastructure.client.dto.response.SendResponse; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/v1/slack/web") +@RequiredArgsConstructor +public class SlackSelectController { + + private final SlackSelectService slackSelectService; + + @GetMapping("/selectAll") + public PageResponse selectAll( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(defaultValue = "LATEST") String sortType + ) { +// Page slackPage = slackSelectService.getSelectAll(page, size, sortType); +// +// +// return PageUtils.toPageResponse(slackPage); + return slackSelectService.getSelectAll(page, size, sortType); + } + +// public ResponseEntity> selectAll( +// @RequestParam(defaultValue = "0") int page, +// @RequestParam(defaultValue = "10") int size, +// @RequestParam(defaultValue = "LATEST") String sortType){ +// Page response = slackSelectService.getSelectAll(page,size,sortType); +// return ResponseEntity.ok(response); +// } +} diff --git a/src/main/java/com/early_express/slack_service/slack/infrastructure/repository/SlackRepositoryCustom.java b/src/main/java/com/early_express/slack_service/slack/infrastructure/repository/SlackRepositoryCustom.java new file mode 100644 index 0000000..946fe4c --- /dev/null +++ b/src/main/java/com/early_express/slack_service/slack/infrastructure/repository/SlackRepositoryCustom.java @@ -0,0 +1,9 @@ +package com.early_express.slack_service.slack.infrastructure.repository; + +import com.early_express.slack_service.slack.infrastructure.client.dto.response.SendResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface SlackRepositoryCustom { + Page slackList(int page, int size,String sortType); +} diff --git a/src/main/java/com/early_express/slack_service/slack/infrastructure/repository/SlackRepositoryImpl.java b/src/main/java/com/early_express/slack_service/slack/infrastructure/repository/SlackRepositoryImpl.java new file mode 100644 index 0000000..3a6900c --- /dev/null +++ b/src/main/java/com/early_express/slack_service/slack/infrastructure/repository/SlackRepositoryImpl.java @@ -0,0 +1,55 @@ +package com.early_express.slack_service.slack.infrastructure.repository; + +import com.early_express.slack_service.slack.domain.entity.QSlack; +import com.early_express.slack_service.slack.infrastructure.client.dto.response.SendResponse; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class SlackRepositoryImpl implements SlackRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public Page slackList(int page, int size,String sortType) { + QSlack slack = QSlack.slack; + long offset = (long)page * size; + + JPAQuery query = queryFactory + .select(Projections.constructor(SendResponse.class, + slack.slackId.id, + slack.receiverSlackId, + slack.message, + slack.type, + slack.status, + slack.sentAt, + slack.errorMessage)) + .from(slack) + .orderBy(slack.sentAt.desc()); + + List list = query + .offset(offset) + .limit(size) + .fetch(); + + + + Long total = queryFactory + .select(slack.count()) + .from(slack) + .fetchOne(); + + return new PageImpl<>(list, PageRequest.of(page,size), total); + } +} + From fac591235b4e4ef58c8c6aef8d34594f5644e40f Mon Sep 17 00:00:00 2001 From: vsuminv Date: Wed, 26 Nov 2025 13:06:07 +0900 Subject: [PATCH 5/7] =?UTF-8?q?fix(service):=20=EC=8A=A4=EC=BC=80=EC=A4=84?= =?UTF-8?q?=EB=A7=81=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +- .../internal/SlackSendService.java | 79 ++++++++----------- .../config/schedule/SchedulerConfig.java | 19 +++++ .../internal/SlackSendController.java | 20 +++-- 4 files changed, 66 insertions(+), 55 deletions(-) diff --git a/build.gradle b/build.gradle index b1f49e7..a0782d2 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,4 @@ + plugins { id 'java' id 'org.springframework.boot' version '3.5.7' @@ -144,4 +145,4 @@ configurations { clean { delete file(querydslDir) -} \ No newline at end of file +} diff --git a/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java b/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java index 2f25165..5370370 100644 --- a/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java +++ b/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java @@ -9,11 +9,14 @@ import com.early_express.slack_service.slack.domain.repository.SlackRepository; import com.early_express.slack_service.slack.infrastructure.client.dto.request.SendRequest; import com.early_express.slack_service.slack.infrastructure.client.dto.response.SendResponse; +import jakarta.annotation.PostConstruct; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; import org.springframework.cache.annotation.CacheEvict; import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.support.CronTrigger; import org.springframework.stereotype.Service; @@ -23,28 +26,25 @@ import static com.early_express.slack_service.global.presentation.exception.GlobalErrorCode.MISSING_PARAMETER; import static com.early_express.slack_service.slack.application.exception.SlackErrorCode.*; + @Service @RequiredArgsConstructor @Transactional public class SlackSendService { private final SlackRepository slackRepository; - private final MessageSend messageSend; - private final TaskScheduler taskScheduler; - + private final MessageSend messageSend; + // 메시지 전송 + DB 저장 public void sendDeliveryMessage(SendRequest sendRequest) throws Exception { if (sendRequest.getReceiverId() == null || sendRequest.getReceiverId().isEmpty()) { throw new SlackException(MISSING_PARAMETER); } + boolean callResult = callSlack(sendRequest); + SlackStatus status = callResult ? SlackStatus.SENT : SlackStatus.FAILED; + String errorMessage = callResult ? null : SLACK_SEND_FAILED.getMessage(); + LocalDateTime sentAt = callResult ? LocalDateTime.now() : null; - boolean call_result = callSlack(sendRequest); - SlackStatus status = call_result ? SlackStatus.SENT : SlackStatus.FAILED; - String errorMessage = call_result ? null : SLACK_SEND_FAILED.getMessage(); - LocalDateTime sentAt = call_result ?LocalDateTime.now(): null ; - - - // DB 저장 Slack slack = Slack.builder() .slackId(SlackId.of()) .receiverSlackId(sendRequest.getReceiverId()) @@ -58,52 +58,37 @@ public void sendDeliveryMessage(SendRequest sendRequest) throws Exception { try { slackRepository.save(slack); - } catch(Exception e){ + } catch (Exception e) { e.printStackTrace(); throw new SlackException(SLACK_DB_SAVE_FAILED); - } } - - // api 호출 - public boolean callSlack( SendRequest sendRequest) throws Exception { - if(messageSend == null) return false; - List id =List.of(sendRequest.getReceiverId()); - boolean result = messageSend.send(id, sendRequest.getMessage()); - - return result; - + public boolean callSlack(SendRequest sendRequest) throws Exception { + if (messageSend == null) return false; + List id = List.of(sendRequest.getReceiverId()); + return messageSend.send(id, sendRequest.getMessage()); } - // 스케줄링 + // 🔹 스케줄러: no-arg, DB 조회 혹은 더미 데이터 생성 + @Scheduled(cron = "0 59 12 * * *") // 매일 12:40 + //@SchedulerLock(name = "sendDeliveryMessageLock") // MSA 환경 안전 @CacheEvict(value = "slack", allEntries = true) - public SendResponse schedule_delivery(SendRequest sendRequest) throws Exception{ - - - Runnable schedule_delivery = new Runnable() - { - @Override - public void run() { - try { - sendDeliveryMessage(sendRequest); - - } catch (Exception e) { - throw new RuntimeException(e); - } + public void scheduleDelivery() { + try { + // 1. DB에서 배송자 리스트 조회 (실제 시나리오) + List receivers = List.of("U09V1GT3BH8"); // 테스트용 더미 데이터 + for (String receiver : receivers) { + SendRequest request = SendRequest.builder() + .receiverId(receiver) + .message("오늘 배송할 주소: 서울시 강남구 ...") // 테스트용 메시지 + .messageType(MessageType.MORNING_DELIVERY) + .build(); + sendDeliveryMessage(request); } - }; - taskScheduler.schedule(schedule_delivery, new CronTrigger("0 39 11 * * *")); - - - - return new SendResponse( - - SlackId.of().getId(), sendRequest.getReceiverId(),sendRequest.getMessage(),sendRequest.getMessageType(),sendRequest.getSlackStatus(),LocalDateTime.now(),""); + } catch (Exception e) { + e.printStackTrace(); + } } - - - } - diff --git a/src/main/java/com/early_express/slack_service/slack/infrastructure/config/schedule/SchedulerConfig.java b/src/main/java/com/early_express/slack_service/slack/infrastructure/config/schedule/SchedulerConfig.java index b2762ee..f3c17ad 100644 --- a/src/main/java/com/early_express/slack_service/slack/infrastructure/config/schedule/SchedulerConfig.java +++ b/src/main/java/com/early_express/slack_service/slack/infrastructure/config/schedule/SchedulerConfig.java @@ -1,19 +1,38 @@ package com.early_express.slack_service.slack.infrastructure.config.schedule; +import net.javacrumbs.shedlock.core.LockProvider; +import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider; +import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import javax.sql.DataSource; + @Configuration @EnableScheduling +@EnableSchedulerLock(defaultLockAtMostFor = "PT5M") // ShedLock 활성화 public class SchedulerConfig { + @Bean public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(5); + scheduler.setThreadNamePrefix("scheduled-task-"); scheduler.initialize(); return scheduler; } + +// @Bean +// public LockProvider lockProvider(DataSource dataSource) { +// return new JdbcTemplateLockProvider( +// JdbcTemplateLockProvider.Configuration.builder() +// .withJdbcTemplate(new JdbcTemplate(dataSource)) +// .usingDbTime() // DB 시간 사용 +// .build() +// ); +// } } diff --git a/src/main/java/com/early_express/slack_service/slack/infrastructure/presentation/internal/SlackSendController.java b/src/main/java/com/early_express/slack_service/slack/infrastructure/presentation/internal/SlackSendController.java index 8e54e75..e87e973 100644 --- a/src/main/java/com/early_express/slack_service/slack/infrastructure/presentation/internal/SlackSendController.java +++ b/src/main/java/com/early_express/slack_service/slack/infrastructure/presentation/internal/SlackSendController.java @@ -18,13 +18,19 @@ public class SlackSendController { private final SlackSendService slackSendService; - @PostMapping("/delivery-sent") - public ResponseEntity sendDeliveryMessage(@RequestBody SendRequest sendRequest) throws Exception { - - SendResponse response =slackSendService.schedule_delivery(sendRequest); - return ResponseEntity.ok(response); - - } +// @PostMapping("/delivery-sent") +// public void sendDeliveryMessage(@RequestBody SendRequest sendRequest) throws Exception { +// +// SendResponse response =slackSendService.schedule_delivery(sendRequest); +// return ResponseEntity.ok(response); +// +// } + + // ai를 통해 정보 받기 (외부호출) +// @PostMapping("/test") +// public void sendTest(@RequestBody SendRequest request) { +// slackSendService.sendDeliveryMessage(request); +// } } From 6b6ddf8187ff389ab49162d5489883fc3db74fd2 Mon Sep 17 00:00:00 2001 From: vsuminv Date: Wed, 26 Nov 2025 14:02:20 +0900 Subject: [PATCH 6/7] =?UTF-8?q?feat(slack)=20:=20=EC=8A=AC=EB=9E=99=20?= =?UTF-8?q?=EB=8B=A8=EA=B1=B4=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/SlackSendService.java | 14 ++------ .../application/web/SlackSelectService.java | 14 ++++++++ .../slack/domain/entity/Slack.java | 8 +++-- .../client/dto/response/SendResponse.java | 36 +++++++++++-------- .../config/schedule/SchedulerConfig.java | 10 +++--- .../web/SlackSelectController.java | 27 ++++++-------- .../repository/SlackRepositoryImpl.java | 2 +- 7 files changed, 60 insertions(+), 51 deletions(-) diff --git a/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java b/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java index 5370370..ab82358 100644 --- a/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java +++ b/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java @@ -8,16 +8,12 @@ import com.early_express.slack_service.slack.domain.entity.SlackStatus; import com.early_express.slack_service.slack.domain.repository.SlackRepository; import com.early_express.slack_service.slack.infrastructure.client.dto.request.SendRequest; -import com.early_express.slack_service.slack.infrastructure.client.dto.response.SendResponse; -import jakarta.annotation.PostConstruct; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; -import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; + import org.springframework.cache.annotation.CacheEvict; -import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.scheduling.support.CronTrigger; import org.springframework.stereotype.Service; import java.time.LocalDateTime; @@ -34,7 +30,6 @@ public class SlackSendService { private final SlackRepository slackRepository; private final MessageSend messageSend; - // 메시지 전송 + DB 저장 public void sendDeliveryMessage(SendRequest sendRequest) throws Exception { if (sendRequest.getReceiverId() == null || sendRequest.getReceiverId().isEmpty()) { throw new SlackException(MISSING_PARAMETER); @@ -46,7 +41,6 @@ public void sendDeliveryMessage(SendRequest sendRequest) throws Exception { LocalDateTime sentAt = callResult ? LocalDateTime.now() : null; Slack slack = Slack.builder() - .slackId(SlackId.of()) .receiverSlackId(sendRequest.getReceiverId()) .message(sendRequest.getMessage()) .type(sendRequest.getMessageType()) @@ -70,13 +64,11 @@ public boolean callSlack(SendRequest sendRequest) throws Exception { return messageSend.send(id, sendRequest.getMessage()); } - // 🔹 스케줄러: no-arg, DB 조회 혹은 더미 데이터 생성 - @Scheduled(cron = "0 59 12 * * *") // 매일 12:40 - //@SchedulerLock(name = "sendDeliveryMessageLock") // MSA 환경 안전 + @Scheduled(cron = "0 1 14 * * *") + //@SchedulerLock(name = "sendDeliveryMessageLock") @CacheEvict(value = "slack", allEntries = true) public void scheduleDelivery() { try { - // 1. DB에서 배송자 리스트 조회 (실제 시나리오) List receivers = List.of("U09V1GT3BH8"); // 테스트용 더미 데이터 for (String receiver : receivers) { SendRequest request = SendRequest.builder() diff --git a/src/main/java/com/early_express/slack_service/slack/application/web/SlackSelectService.java b/src/main/java/com/early_express/slack_service/slack/application/web/SlackSelectService.java index 0da4ce4..c72d71e 100644 --- a/src/main/java/com/early_express/slack_service/slack/application/web/SlackSelectService.java +++ b/src/main/java/com/early_express/slack_service/slack/application/web/SlackSelectService.java @@ -3,7 +3,10 @@ import com.early_express.slack_service.global.common.utils.PageUtils; import com.early_express.slack_service.global.presentation.dto.PageResponse; +import com.early_express.slack_service.slack.application.exception.SlackErrorCode; +import com.early_express.slack_service.slack.application.exception.SlackException; import com.early_express.slack_service.slack.domain.entity.Slack; +import com.early_express.slack_service.slack.domain.entity.SlackId; import com.early_express.slack_service.slack.domain.repository.SlackRepository; import com.early_express.slack_service.slack.infrastructure.client.dto.response.SendResponse; import com.early_express.slack_service.slack.infrastructure.repository.SlackRepositoryImpl; @@ -15,12 +18,14 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import java.util.UUID; import java.util.function.Function; @Service @RequiredArgsConstructor public class SlackSelectService { private final SlackRepositoryImpl slackRepositoryImpl; + private final SlackRepository slackRepository; @Transactional(readOnly = true) @Cacheable(value = "slack", key = " #page + ':' + #size + ':' + #sortType") @@ -30,4 +35,13 @@ public PageResponse getSelectAll(int page, int size, String sortTy return PageUtils.toPageResponse(slackPage); } + + @Cacheable(cacheNames = "slack", key = "'oneSlackResponse'", cacheManager = "cacheManager") + public SendResponse getOneSlack(UUID id) { + + + Slack slack = slackRepository.findById(id).orElseThrow(() -> new SlackException(SlackErrorCode.SLACK_NOT_FOUND)); + + return SendResponse.of(slack); + } } diff --git a/src/main/java/com/early_express/slack_service/slack/domain/entity/Slack.java b/src/main/java/com/early_express/slack_service/slack/domain/entity/Slack.java index 0da3c25..6dbe934 100644 --- a/src/main/java/com/early_express/slack_service/slack/domain/entity/Slack.java +++ b/src/main/java/com/early_express/slack_service/slack/domain/entity/Slack.java @@ -6,7 +6,6 @@ import lombok.*; import java.time.LocalDateTime; -import java.util.List; import java.util.UUID; @Table(name = "slack") @@ -19,8 +18,11 @@ @AllArgsConstructor(access = AccessLevel.PROTECTED) public class Slack extends BaseEntity { - @EmbeddedId - private SlackId slackId; + @Id + @Column(columnDefinition = "BINARY(16)") + @GeneratedValue(strategy = GenerationType.UUID) +// private SlackId slackId; + private UUID slackId; @Column(nullable=false) private String receiverSlackId; diff --git a/src/main/java/com/early_express/slack_service/slack/infrastructure/client/dto/response/SendResponse.java b/src/main/java/com/early_express/slack_service/slack/infrastructure/client/dto/response/SendResponse.java index d9ae200..5fa7439 100644 --- a/src/main/java/com/early_express/slack_service/slack/infrastructure/client/dto/response/SendResponse.java +++ b/src/main/java/com/early_express/slack_service/slack/infrastructure/client/dto/response/SendResponse.java @@ -2,29 +2,35 @@ import com.early_express.slack_service.slack.domain.entity.MessageType; +import com.early_express.slack_service.slack.domain.entity.Slack; import com.early_express.slack_service.slack.domain.entity.SlackStatus; -import com.early_express.slack_service.slack.infrastructure.client.dto.request.SendRequest; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import java.io.Serializable; import java.time.LocalDateTime; import java.util.UUID; -@Getter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class SendResponse { - private UUID slackId; - private String receiverSlackId; - private String message; - private MessageType messageType; - private SlackStatus status; - private LocalDateTime sentAt; - private String errorMessage; - - +public record SendResponse ( + UUID slackId, + String receiverSlackId, + String message, + MessageType messageType, + SlackStatus status, + LocalDateTime sentAt, + String errorMessage +) { + public static SendResponse of(Slack slack) { + return SendResponse.builder() + .slackId(slack.getSlackId()) + .receiverSlackId(slack.getReceiverSlackId()) + .message(slack.getMessage()) + .messageType(slack.getType()) + .status(slack.getStatus()) + .sentAt(slack.getSentAt()) + .errorMessage(slack.getErrorMessage()) + .build(); + } } diff --git a/src/main/java/com/early_express/slack_service/slack/infrastructure/config/schedule/SchedulerConfig.java b/src/main/java/com/early_express/slack_service/slack/infrastructure/config/schedule/SchedulerConfig.java index f3c17ad..88e5574 100644 --- a/src/main/java/com/early_express/slack_service/slack/infrastructure/config/schedule/SchedulerConfig.java +++ b/src/main/java/com/early_express/slack_service/slack/infrastructure/config/schedule/SchedulerConfig.java @@ -1,11 +1,11 @@ package com.early_express.slack_service.slack.infrastructure.config.schedule; -import net.javacrumbs.shedlock.core.LockProvider; -import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider; -import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; +//import net.javacrumbs.shedlock.core.LockProvider; +//import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider; +//import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.jdbc.core.JdbcTemplate; +//import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; @@ -14,7 +14,7 @@ @Configuration @EnableScheduling -@EnableSchedulerLock(defaultLockAtMostFor = "PT5M") // ShedLock 활성화 +//@EnableSchedulerLock(defaultLockAtMostFor = "PT5M") // ShedLock 활성화 public class SchedulerConfig { @Bean diff --git a/src/main/java/com/early_express/slack_service/slack/infrastructure/presentation/web/SlackSelectController.java b/src/main/java/com/early_express/slack_service/slack/infrastructure/presentation/web/SlackSelectController.java index 0cbf117..3c5bd09 100644 --- a/src/main/java/com/early_express/slack_service/slack/infrastructure/presentation/web/SlackSelectController.java +++ b/src/main/java/com/early_express/slack_service/slack/infrastructure/presentation/web/SlackSelectController.java @@ -10,10 +10,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; @RestController @RequestMapping("/v1/slack/web") @@ -22,24 +21,20 @@ public class SlackSelectController { private final SlackSelectService slackSelectService; - @GetMapping("/selectAll") + @GetMapping("/select") public PageResponse selectAll( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(defaultValue = "LATEST") String sortType ) { -// Page slackPage = slackSelectService.getSelectAll(page, size, sortType); -// -// -// return PageUtils.toPageResponse(slackPage); return slackSelectService.getSelectAll(page, size, sortType); } -// public ResponseEntity> selectAll( -// @RequestParam(defaultValue = "0") int page, -// @RequestParam(defaultValue = "10") int size, -// @RequestParam(defaultValue = "LATEST") String sortType){ -// Page response = slackSelectService.getSelectAll(page,size,sortType); -// return ResponseEntity.ok(response); -// } + @GetMapping("/select/{id}") + public ResponseEntity select(@PathVariable UUID id) { + SendResponse response = slackSelectService.getOneSlack(id); + return ResponseEntity.ok(response); + + } + } diff --git a/src/main/java/com/early_express/slack_service/slack/infrastructure/repository/SlackRepositoryImpl.java b/src/main/java/com/early_express/slack_service/slack/infrastructure/repository/SlackRepositoryImpl.java index 3a6900c..d131cc2 100644 --- a/src/main/java/com/early_express/slack_service/slack/infrastructure/repository/SlackRepositoryImpl.java +++ b/src/main/java/com/early_express/slack_service/slack/infrastructure/repository/SlackRepositoryImpl.java @@ -27,7 +27,7 @@ public Page slackList(int page, int size,String sortType) { JPAQuery query = queryFactory .select(Projections.constructor(SendResponse.class, - slack.slackId.id, + slack.slackId, slack.receiverSlackId, slack.message, slack.type, From b5e077be1a615299136d748a5de6f33cefd2e0e6 Mon Sep 17 00:00:00 2001 From: vsuminv Date: Wed, 26 Nov 2025 14:05:15 +0900 Subject: [PATCH 7/7] =?UTF-8?q?refactor=20:=20=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../slack/application/internal/SlackSendService.java | 5 ++--- .../slack/application/web/SlackSelectService.java | 4 ---- .../slack_service/slack/domain/entity/Slack.java | 1 - .../slack/domain/repository/SlackRepository.java | 1 - .../infrastructure/config/schedule/SchedulerConfig.java | 9 --------- .../infrastructure/repository/SlackRepositoryImpl.java | 1 - 6 files changed, 2 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java b/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java index ab82358..6ef03f6 100644 --- a/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java +++ b/src/main/java/com/early_express/slack_service/slack/application/internal/SlackSendService.java @@ -4,7 +4,6 @@ import com.early_express.slack_service.slack.domain.MessageSend; import com.early_express.slack_service.slack.domain.entity.MessageType; import com.early_express.slack_service.slack.domain.entity.Slack; -import com.early_express.slack_service.slack.domain.entity.SlackId; import com.early_express.slack_service.slack.domain.entity.SlackStatus; import com.early_express.slack_service.slack.domain.repository.SlackRepository; import com.early_express.slack_service.slack.infrastructure.client.dto.request.SendRequest; @@ -69,11 +68,11 @@ public boolean callSlack(SendRequest sendRequest) throws Exception { @CacheEvict(value = "slack", allEntries = true) public void scheduleDelivery() { try { - List receivers = List.of("U09V1GT3BH8"); // 테스트용 더미 데이터 + List receivers = List.of("U09V1GT3BH8"); for (String receiver : receivers) { SendRequest request = SendRequest.builder() .receiverId(receiver) - .message("오늘 배송할 주소: 서울시 강남구 ...") // 테스트용 메시지 + .message("오늘 배송할 주소: 서울시 강남구 ...") .messageType(MessageType.MORNING_DELIVERY) .build(); sendDeliveryMessage(request); diff --git a/src/main/java/com/early_express/slack_service/slack/application/web/SlackSelectService.java b/src/main/java/com/early_express/slack_service/slack/application/web/SlackSelectService.java index c72d71e..5c484c8 100644 --- a/src/main/java/com/early_express/slack_service/slack/application/web/SlackSelectService.java +++ b/src/main/java/com/early_express/slack_service/slack/application/web/SlackSelectService.java @@ -6,7 +6,6 @@ import com.early_express.slack_service.slack.application.exception.SlackErrorCode; import com.early_express.slack_service.slack.application.exception.SlackException; import com.early_express.slack_service.slack.domain.entity.Slack; -import com.early_express.slack_service.slack.domain.entity.SlackId; import com.early_express.slack_service.slack.domain.repository.SlackRepository; import com.early_express.slack_service.slack.infrastructure.client.dto.response.SendResponse; import com.early_express.slack_service.slack.infrastructure.repository.SlackRepositoryImpl; @@ -14,12 +13,9 @@ import org.springframework.cache.annotation.Cacheable; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import java.util.UUID; -import java.util.function.Function; @Service @RequiredArgsConstructor diff --git a/src/main/java/com/early_express/slack_service/slack/domain/entity/Slack.java b/src/main/java/com/early_express/slack_service/slack/domain/entity/Slack.java index 6dbe934..8c791e8 100644 --- a/src/main/java/com/early_express/slack_service/slack/domain/entity/Slack.java +++ b/src/main/java/com/early_express/slack_service/slack/domain/entity/Slack.java @@ -21,7 +21,6 @@ public class Slack extends BaseEntity { @Id @Column(columnDefinition = "BINARY(16)") @GeneratedValue(strategy = GenerationType.UUID) -// private SlackId slackId; private UUID slackId; @Column(nullable=false) diff --git a/src/main/java/com/early_express/slack_service/slack/domain/repository/SlackRepository.java b/src/main/java/com/early_express/slack_service/slack/domain/repository/SlackRepository.java index ccf0e21..c80df80 100644 --- a/src/main/java/com/early_express/slack_service/slack/domain/repository/SlackRepository.java +++ b/src/main/java/com/early_express/slack_service/slack/domain/repository/SlackRepository.java @@ -14,7 +14,6 @@ @Repository public interface SlackRepository extends JpaRepository, SlackRepositoryCustom { - //Page slackList(Pageable pageable); Page slackList(int page, int size,String sortType); } diff --git a/src/main/java/com/early_express/slack_service/slack/infrastructure/config/schedule/SchedulerConfig.java b/src/main/java/com/early_express/slack_service/slack/infrastructure/config/schedule/SchedulerConfig.java index 88e5574..36ee5f3 100644 --- a/src/main/java/com/early_express/slack_service/slack/infrastructure/config/schedule/SchedulerConfig.java +++ b/src/main/java/com/early_express/slack_service/slack/infrastructure/config/schedule/SchedulerConfig.java @@ -26,13 +26,4 @@ public TaskScheduler taskScheduler() { return scheduler; } -// @Bean -// public LockProvider lockProvider(DataSource dataSource) { -// return new JdbcTemplateLockProvider( -// JdbcTemplateLockProvider.Configuration.builder() -// .withJdbcTemplate(new JdbcTemplate(dataSource)) -// .usingDbTime() // DB 시간 사용 -// .build() -// ); -// } } diff --git a/src/main/java/com/early_express/slack_service/slack/infrastructure/repository/SlackRepositoryImpl.java b/src/main/java/com/early_express/slack_service/slack/infrastructure/repository/SlackRepositoryImpl.java index d131cc2..9287184 100644 --- a/src/main/java/com/early_express/slack_service/slack/infrastructure/repository/SlackRepositoryImpl.java +++ b/src/main/java/com/early_express/slack_service/slack/infrastructure/repository/SlackRepositoryImpl.java @@ -9,7 +9,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; import java.util.List;