From c83ed0d033badd3ea9ce75e890830e20eba91045 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 11 Jun 2025 17:56:39 +0900 Subject: [PATCH] fix: ai-recommend build --- .../AIRecommendServiceApplication.java | 14 +- .../application/service/AiApiService.java | 21 +++ .../service/MarketingTipService.java | 106 +++++++++---- .../service/WeatherDataService.java | 32 ++++ .../usecase/MarketingTipUseCase.java | 31 +++- .../recommend/config/CacheConfig.java | 13 ++ .../recommend/config/JpaConfig.java | 12 ++ .../recommend/config/WebClientConfig.java | 33 ++++ .../domain/model/BusinessInsight.java | 51 ++++++ .../recommend/domain/model/MarketingTip.java | 54 ++----- .../recommend/domain/model/StoreData.java | 61 +------- .../recommend/domain/model/TipId.java | 24 +-- .../recommend/domain/model/WeatherData.java | 61 +------- .../repository/BusinessInsightRepository.java | 15 ++ .../repository/MarketingTipRepository.java | 55 ++----- .../domain/service/StoreDataProvider.java | 8 +- .../domain/service/WeatherDataProvider.java | 6 +- .../external/AiApiServiceImpl.java | 137 +++++++++++++++++ .../external/ClaudeAiTipGenerator.java | 2 +- .../external/PythonAiTipGenerator.java | 137 +++++++++++++++++ .../external/StoreApiDataProvider.java | 136 ++++++++-------- .../external/WeatherApiDataProvider.java | 145 ++++-------------- .../JpaMarketingTipRepository.java | 38 +++++ .../persistence/MarketingTipEntity.java | 58 +++++++ .../MarketingTipJpaRepository.java | 14 ++ .../controller/RecommendationController.java | 66 ++++++-- .../presentation/dto/AIServiceRequest.java | 24 +++ .../presentation/dto/MarketingTipRequest.java | 22 +-- .../dto/MarketingTipResponse.java | 63 ++++++-- .../src/main/resources/application.yml | 13 +- 30 files changed, 977 insertions(+), 475 deletions(-) create mode 100644 smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/application/service/AiApiService.java create mode 100644 smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/application/service/WeatherDataService.java create mode 100644 smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/config/CacheConfig.java create mode 100644 smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/config/JpaConfig.java create mode 100644 smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/config/WebClientConfig.java create mode 100644 smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/model/BusinessInsight.java create mode 100644 smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/repository/BusinessInsightRepository.java create mode 100644 smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/AiApiServiceImpl.java create mode 100644 smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/PythonAiTipGenerator.java create mode 100644 smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/persistence/JpaMarketingTipRepository.java create mode 100644 smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/persistence/MarketingTipEntity.java create mode 100644 smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/persistence/MarketingTipJpaRepository.java create mode 100644 smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/dto/AIServiceRequest.java diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/AIRecommendServiceApplication.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/AIRecommendServiceApplication.java index 6ebb3f5..2c12c85 100644 --- a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/AIRecommendServiceApplication.java +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/AIRecommendServiceApplication.java @@ -2,18 +2,16 @@ package com.won.smarketing.recommend; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.domain.EntityScan; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; /** - * AI 추천 서비스 메인 애플리케이션 클래스 - * Clean Architecture 패턴을 적용한 AI 마케팅 추천 서비스 + * AI 추천 서비스 메인 애플리케이션 */ -@SpringBootApplication(scanBasePackages = {"com.won.smarketing.recommend", "com.won.smarketing.common"}) -@EntityScan(basePackages = {"com.won.smarketing.recommend.infrastructure.entity"}) -@EnableJpaRepositories(basePackages = {"com.won.smarketing.recommend.infrastructure.repository"}) +@SpringBootApplication +@EnableJpaAuditing +@EnableCaching public class AIRecommendServiceApplication { - public static void main(String[] args) { SpringApplication.run(AIRecommendServiceApplication.class, args); } diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/application/service/AiApiService.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/application/service/AiApiService.java new file mode 100644 index 0000000..30338a0 --- /dev/null +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/application/service/AiApiService.java @@ -0,0 +1,21 @@ +package com.won.smarketing.recommend.domain.service; + +import com.won.smarketing.recommend.domain.model.StoreData; +import com.won.smarketing.recommend.domain.model.WeatherData; + +/** + * Python AI 서비스 인터페이스 + * AI 처리를 Python 서비스로 위임하는 도메인 서비스 + */ +public interface AiApiService { + + /** + * Python AI 서비스를 통한 마케팅 팁 생성 + * + * @param storeData 매장 정보 + * @param weatherData 날씨 정보 + * @param additionalRequirement 추가 요청사항 + * @return AI가 생성한 마케팅 팁 (한 줄) + */ + String generateMarketingTip(StoreData storeData, WeatherData weatherData, String additionalRequirement); +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/application/service/MarketingTipService.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/application/service/MarketingTipService.java index 7d80205..67193b9 100644 --- a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/application/service/MarketingTipService.java +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/application/service/MarketingTipService.java @@ -8,26 +8,26 @@ import com.won.smarketing.recommend.domain.model.StoreData; import com.won.smarketing.recommend.domain.model.TipId; import com.won.smarketing.recommend.domain.model.WeatherData; import com.won.smarketing.recommend.domain.repository.MarketingTipRepository; -import com.won.smarketing.recommend.domain.service.AiTipGenerator; import com.won.smarketing.recommend.domain.service.StoreDataProvider; import com.won.smarketing.recommend.domain.service.WeatherDataProvider; +import com.won.smarketing.recommend.domain.service.AiTipGenerator; import com.won.smarketing.recommend.presentation.dto.MarketingTipRequest; import com.won.smarketing.recommend.presentation.dto.MarketingTipResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; - /** * 마케팅 팁 서비스 구현체 - * AI 기반 마케팅 팁 생성 및 저장 기능 구현 */ @Slf4j @Service @RequiredArgsConstructor -@Transactional(readOnly = true) +@Transactional public class MarketingTipService implements MarketingTipUseCase { private final MarketingTipRepository marketingTipRepository; @@ -35,49 +35,95 @@ public class MarketingTipService implements MarketingTipUseCase { private final WeatherDataProvider weatherDataProvider; private final AiTipGenerator aiTipGenerator; - /** - * AI 마케팅 팁 생성 - * - * @param request 마케팅 팁 생성 요청 - * @return 생성된 마케팅 팁 응답 - */ @Override - @Transactional public MarketingTipResponse generateMarketingTips(MarketingTipRequest request) { + log.info("마케팅 팁 생성 시작: storeId={}", request.getStoreId()); + try { - // 매장 정보 조회 + // 1. 매장 정보 조회 StoreData storeData = storeDataProvider.getStoreData(request.getStoreId()); log.debug("매장 정보 조회 완료: {}", storeData.getStoreName()); - // 날씨 정보 조회 + // 2. 날씨 정보 조회 WeatherData weatherData = weatherDataProvider.getCurrentWeather(storeData.getLocation()); - log.debug("날씨 정보 조회 완료: {} 도", weatherData.getTemperature()); + log.debug("날씨 정보 조회 완료: 온도={}, 상태={}", weatherData.getTemperature(), weatherData.getCondition()); - // AI를 사용하여 마케팅 팁 생성 - String tipContent = aiTipGenerator.generateTip(storeData, weatherData); - log.debug("AI 마케팅 팁 생성 완료"); + // 3. AI 팁 생성 + String aiGeneratedTip = aiTipGenerator.generateTip(storeData, weatherData, request.getAdditionalRequirement()); + log.debug("AI 팁 생성 완료: {}", aiGeneratedTip.substring(0, Math.min(50, aiGeneratedTip.length()))); - // 마케팅 팁 도메인 객체 생성 + // 4. 도메인 객체 생성 및 저장 MarketingTip marketingTip = MarketingTip.builder() .storeId(request.getStoreId()) - .tipContent(tipContent) + .tipContent(aiGeneratedTip) .weatherData(weatherData) .storeData(storeData) - .createdAt(LocalDateTime.now()) .build(); - // 마케팅 팁 저장 MarketingTip savedTip = marketingTipRepository.save(marketingTip); + log.info("마케팅 팁 저장 완료: tipId={}", savedTip.getId().getValue()); - return MarketingTipResponse.builder() - .tipId(savedTip.getId().getValue()) - .tipContent(savedTip.getTipContent()) - .createdAt(savedTip.getCreatedAt()) - .build(); + return convertToResponse(savedTip); } catch (Exception e) { - log.error("마케팅 팁 생성 중 오류 발생", e); - throw new BusinessException(ErrorCode.RECOMMENDATION_FAILED); + log.error("마케팅 팁 생성 중 오류: storeId={}", request.getStoreId(), e); + throw new BusinessException(ErrorCode.AI_TIP_GENERATION_FAILED); } } -} + + @Override + @Transactional(readOnly = true) + @Cacheable(value = "marketingTipHistory", key = "#storeId + '_' + #pageable.pageNumber + '_' + #pageable.pageSize") + public Page getMarketingTipHistory(Long storeId, Pageable pageable) { + log.info("마케팅 팁 이력 조회: storeId={}", storeId); + + Page tips = marketingTipRepository.findByStoreIdOrderByCreatedAtDesc(storeId, pageable); + + return tips.map(this::convertToResponse); + } + + @Override + @Transactional(readOnly = true) + public MarketingTipResponse getMarketingTip(Long tipId) { + log.info("마케팅 팁 상세 조회: tipId={}", tipId); + + MarketingTip marketingTip = marketingTipRepository.findById(tipId) + .orElseThrow(() -> new BusinessException(ErrorCode.MARKETING_TIP_NOT_FOUND)); + + return convertToResponse(marketingTip); + } + + private MarketingTipResponse convertToResponse(MarketingTip marketingTip) { + return MarketingTipResponse.builder() + .tipId(marketingTip.getId().getValue()) + .storeId(marketingTip.getStoreId()) + .storeName(marketingTip.getStoreData().getStoreName()) + .businessType(marketingTip.getStoreData().getBusinessType()) + .storeLocation(marketingTip.getStoreData().getLocation()) + .createdAt(marketingTip.getCreatedAt()) + .build(); + } + + public MarketingTip toDomain() { + WeatherData weatherData = WeatherData.builder() + .temperature(this.weatherTemperature) + .condition(this.weatherCondition) + .humidity(this.weatherHumidity) + .build(); + + StoreData storeData = StoreData.builder() + .storeName(this.storeName) + .businessType(this.businessType) + .location(this.storeLocation) + .build(); + + return MarketingTip.builder() + .id(this.id != null ? TipId.of(this.id) : null) + .storeId(this.storeId) + .tipContent(this.tipContent) + .weatherData(weatherData) + .storeData(storeData) + .createdAt(this.createdAt) + .build(); + } +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/application/service/WeatherDataService.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/application/service/WeatherDataService.java new file mode 100644 index 0000000..164a1a9 --- /dev/null +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/application/service/WeatherDataService.java @@ -0,0 +1,32 @@ +package com.won.smarketing.recommend.application.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +/** + * 날씨 데이터 서비스 (Mock) + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WeatherDataService { + + @Cacheable(value = "weatherData", key = "#location") + public com.won.smarketing.recommend.service.MarketingTipService.WeatherInfo getCurrentWeather(String location) { + log.debug("날씨 정보 조회: location={}", location); + + // Mock 데이터 반환 + double temperature = 20.0 + (Math.random() * 15); // 20-35도 + String[] conditions = {"맑음", "흐림", "비", "눈", "안개"}; + String condition = conditions[(int) (Math.random() * conditions.length)]; + double humidity = 50.0 + (Math.random() * 30); // 50-80% + + return com.won.smarketing.recommend.service.MarketingTipService.WeatherInfo.builder() + .temperature(Math.round(temperature * 10) / 10.0) + .condition(condition) + .humidity(Math.round(humidity * 10) / 10.0) + .build(); + } +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/application/usecase/MarketingTipUseCase.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/application/usecase/MarketingTipUseCase.java index b5e6598..b1a8329 100644 --- a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/application/usecase/MarketingTipUseCase.java +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/application/usecase/MarketingTipUseCase.java @@ -2,18 +2,37 @@ package com.won.smarketing.recommend.application.usecase; import com.won.smarketing.recommend.presentation.dto.MarketingTipRequest; import com.won.smarketing.recommend.presentation.dto.MarketingTipResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; /** - * 마케팅 팁 관련 Use Case 인터페이스 - * AI 기반 마케팅 팁 생성 기능 정의 + * 마케팅 팁 생성 유즈케이스 인터페이스 + * 비즈니스 요구사항을 정의하는 애플리케이션 계층의 인터페이스 */ public interface MarketingTipUseCase { - + /** * AI 마케팅 팁 생성 - * + * * @param request 마케팅 팁 생성 요청 - * @return 생성된 마케팅 팁 응답 + * @return 생성된 마케팅 팁 정보 */ MarketingTipResponse generateMarketingTips(MarketingTipRequest request); -} + + /** + * 마케팅 팁 이력 조회 + * + * @param storeId 매장 ID + * @param pageable 페이징 정보 + * @return 마케팅 팁 이력 페이지 + */ + Page getMarketingTipHistory(Long storeId, Pageable pageable); + + /** + * 마케팅 팁 상세 조회 + * + * @param tipId 팁 ID + * @return 마케팅 팁 상세 정보 + */ + MarketingTipResponse getMarketingTip(Long tipId); +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/config/CacheConfig.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/config/CacheConfig.java new file mode 100644 index 0000000..9aec563 --- /dev/null +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/config/CacheConfig.java @@ -0,0 +1,13 @@ +package com.won.smarketing.recommend.config; + +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Configuration; + +/** + * 캐시 설정 + */ +@Configuration +@EnableCaching +public class CacheConfig { + // 기본 Simple 캐시 사용 +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/config/JpaConfig.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/config/JpaConfig.java new file mode 100644 index 0000000..de705f5 --- /dev/null +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/config/JpaConfig.java @@ -0,0 +1,12 @@ +package com.won.smarketing.recommend.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +/** + * JPA 설정 + */ +@Configuration +@EnableJpaRepositories +public class JpaConfig { +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/config/WebClientConfig.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/config/WebClientConfig.java new file mode 100644 index 0000000..47ed442 --- /dev/null +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/config/WebClientConfig.java @@ -0,0 +1,33 @@ +package com.won.smarketing.recommend.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; +import io.netty.channel.ChannelOption; +import io.netty.handler.timeout.ConnectTimeoutHandler; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import reactor.netty.http.client.HttpClient; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +/** + * WebClient 설정 + */ +@Configuration +public class WebClientConfig { + + @Bean + public WebClient webClient() { + HttpClient httpClient = HttpClient.create() + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + .responseTimeout(Duration.ofMillis(5000)) + .doOnConnected(conn -> conn + .addHandlerLast(new ConnectTimeoutHandler(5, TimeUnit.SECONDS))); + + return WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024)) + .build(); + } +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/model/BusinessInsight.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/model/BusinessInsight.java new file mode 100644 index 0000000..5022134 --- /dev/null +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/model/BusinessInsight.java @@ -0,0 +1,51 @@ +package com.won.smarketing.recommend.domain.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +/** + * 비즈니스 인사이트 엔티티 + */ +@Entity +@Table(name = "business_insights") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EntityListeners(AuditingEntityListener.class) +public class BusinessInsight { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "insight_id") + private Long id; + + @Column(name = "store_id", nullable = false) + private Long storeId; + + @Column(name = "insight_type", nullable = false, length = 50) + private String insightType; + + @Column(name = "title", nullable = false, length = 200) + private String title; + + @Column(name = "description", columnDefinition = "TEXT") + private String description; + + @Column(name = "metric_value") + private Double metricValue; + + @Column(name = "recommendation", columnDefinition = "TEXT") + private String recommendation; + + @CreatedDate + @Column(name = "created_at") + private LocalDateTime createdAt; +} diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/model/MarketingTip.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/model/MarketingTip.java index 8ff523d..48bc27b 100644 --- a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/model/MarketingTip.java +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/model/MarketingTip.java @@ -1,58 +1,38 @@ package com.won.smarketing.recommend.domain.model; -import lombok.*; +import com.won.smarketing.recommend.domain.model.StoreData; +import com.won.smarketing.recommend.domain.model.TipId; +import com.won.smarketing.recommend.domain.model.WeatherData; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; import java.time.LocalDateTime; /** * 마케팅 팁 도메인 모델 - * AI가 생성한 마케팅 팁과 관련 정보를 관리 */ @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor @Builder +@NoArgsConstructor +@AllArgsConstructor public class MarketingTip { - /** - * 마케팅 팁 고유 식별자 - */ private TipId id; - - /** - * 매장 ID - */ private Long storeId; - - /** - * AI가 생성한 마케팅 팁 내용 - */ private String tipContent; - - /** - * 팁 생성 시 참고한 날씨 데이터 - */ private WeatherData weatherData; - - /** - * 팁 생성 시 참고한 매장 데이터 - */ private StoreData storeData; - - /** - * 팁 생성 시각 - */ private LocalDateTime createdAt; - /** - * 팁 내용 업데이트 - * - * @param newContent 새로운 팁 내용 - */ - public void updateContent(String newContent) { - if (newContent == null || newContent.trim().isEmpty()) { - throw new IllegalArgumentException("팁 내용은 비어있을 수 없습니다."); - } - this.tipContent = newContent.trim(); + public static MarketingTip create(Long storeId, String tipContent, WeatherData weatherData, StoreData storeData) { + return MarketingTip.builder() + .storeId(storeId) + .tipContent(tipContent) + .weatherData(weatherData) + .storeData(storeData) + .createdAt(LocalDateTime.now()) + .build(); } } \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/model/StoreData.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/model/StoreData.java index 0f38f43..2afae1b 100644 --- a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/model/StoreData.java +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/model/StoreData.java @@ -1,66 +1,19 @@ package com.won.smarketing.recommend.domain.model; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; /** * 매장 데이터 값 객체 - * 마케팅 팁 생성에 사용되는 매장 정보 */ @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor @Builder -@EqualsAndHashCode +@NoArgsConstructor +@AllArgsConstructor public class StoreData { - - /** - * 매장명 - */ private String storeName; - - /** - * 업종 - */ private String businessType; - - /** - * 매장 위치 (주소) - */ private String location; - - /** - * 매장 데이터 유효성 검증 - * - * @return 유효성 여부 - */ - public boolean isValid() { - return storeName != null && !storeName.trim().isEmpty() && - businessType != null && !businessType.trim().isEmpty() && - location != null && !location.trim().isEmpty(); - } - - /** - * 업종 카테고리 분류 - * - * @return 업종 카테고리 - */ - public String getBusinessCategory() { - if (businessType == null) { - return "기타"; - } - - String lowerCaseType = businessType.toLowerCase(); - - if (lowerCaseType.contains("카페") || lowerCaseType.contains("커피")) { - return "카페"; - } else if (lowerCaseType.contains("식당") || lowerCaseType.contains("레스토랑")) { - return "음식점"; - } else if (lowerCaseType.contains("베이커리") || lowerCaseType.contains("빵")) { - return "베이커리"; - } else if (lowerCaseType.contains("치킨") || lowerCaseType.contains("피자")) { - return "패스트푸드"; - } else { - return "기타"; - } - } -} +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/model/TipId.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/model/TipId.java index ae0b1df..105b3af 100644 --- a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/model/TipId.java +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/model/TipId.java @@ -1,29 +1,21 @@ package com.won.smarketing.recommend.domain.model; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; /** - * 마케팅 팁 식별자 값 객체 - * 마케팅 팁의 고유 식별자를 나타내는 도메인 객체 + * 팁 ID 값 객체 */ @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) @EqualsAndHashCode +@NoArgsConstructor +@AllArgsConstructor public class TipId { - private Long value; - /** - * TipId 생성 팩토리 메서드 - * - * @param value 식별자 값 - * @return TipId 인스턴스 - */ public static TipId of(Long value) { - if (value == null || value <= 0) { - throw new IllegalArgumentException("TipId는 양수여야 합니다."); - } return new TipId(value); } -} +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/model/WeatherData.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/model/WeatherData.java index c1d4f54..90c6455 100644 --- a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/model/WeatherData.java +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/model/WeatherData.java @@ -1,66 +1,19 @@ package com.won.smarketing.recommend.domain.model; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; /** * 날씨 데이터 값 객체 - * 마케팅 팁 생성에 사용되는 날씨 정보 */ @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor @Builder -@EqualsAndHashCode +@NoArgsConstructor +@AllArgsConstructor public class WeatherData { - - /** - * 온도 (섭씨) - */ private Double temperature; - - /** - * 날씨 상태 (맑음, 흐림, 비, 눈 등) - */ private String condition; - - /** - * 습도 (%) - */ private Double humidity; - - /** - * 날씨 데이터 유효성 검증 - * - * @return 유효성 여부 - */ - public boolean isValid() { - return temperature != null && - condition != null && !condition.trim().isEmpty() && - humidity != null && humidity >= 0 && humidity <= 100; - } - - /** - * 온도 기반 날씨 상태 설명 - * - * @return 날씨 상태 설명 - */ - public String getTemperatureDescription() { - if (temperature == null) { - return "알 수 없음"; - } - - if (temperature >= 30) { - return "매우 더움"; - } else if (temperature >= 25) { - return "더움"; - } else if (temperature >= 20) { - return "따뜻함"; - } else if (temperature >= 10) { - return "선선함"; - } else if (temperature >= 0) { - return "춥다"; - } else { - return "매우 춥다"; - } - } -} +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/repository/BusinessInsightRepository.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/repository/BusinessInsightRepository.java new file mode 100644 index 0000000..9925144 --- /dev/null +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/repository/BusinessInsightRepository.java @@ -0,0 +1,15 @@ +package com.won.smarketing.recommend.domain.repository; + +import com.won.smarketing.recommend.domain.model.BusinessInsight; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface BusinessInsightRepository extends JpaRepository { + + List findByStoreIdOrderByCreatedAtDesc(Long storeId); + + List findByInsightTypeAndStoreId(String insightType, Long storeId); +} diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/repository/MarketingTipRepository.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/repository/MarketingTipRepository.java index fd5e537..140dff3 100644 --- a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/repository/MarketingTipRepository.java +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/repository/MarketingTipRepository.java @@ -1,56 +1,19 @@ package com.won.smarketing.recommend.domain.repository; import com.won.smarketing.recommend.domain.model.MarketingTip; -import com.won.smarketing.recommend.domain.model.TipId; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; -import java.time.LocalDateTime; -import java.util.List; import java.util.Optional; /** - * 마케팅 팁 저장소 인터페이스 - * 마케팅 팁 도메인의 데이터 접근 추상화 + * 마케팅 팁 레포지토리 인터페이스 */ public interface MarketingTipRepository { - - /** - * 마케팅 팁 저장 - * - * @param marketingTip 저장할 마케팅 팁 - * @return 저장된 마케팅 팁 - */ + MarketingTip save(MarketingTip marketingTip); - - /** - * 마케팅 팁 ID로 조회 - * - * @param id 마케팅 팁 ID - * @return 마케팅 팁 (Optional) - */ - Optional findById(TipId id); - - /** - * 매장별 마케팅 팁 목록 조회 - * - * @param storeId 매장 ID - * @return 마케팅 팁 목록 - */ - List findByStoreId(Long storeId); - - /** - * 특정 기간 내 생성된 마케팅 팁 조회 - * - * @param storeId 매장 ID - * @param startDate 시작 시각 - * @param endDate 종료 시각 - * @return 마케팅 팁 목록 - */ - List findByStoreIdAndCreatedAtBetween(Long storeId, LocalDateTime startDate, LocalDateTime endDate); - - /** - * 마케팅 팁 삭제 - * - * @param id 삭제할 마케팅 팁 ID - */ - void deleteById(TipId id); -} + + Optional findById(Long tipId); + + Page findByStoreIdOrderByCreatedAtDesc(Long storeId, Pageable pageable); +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/service/StoreDataProvider.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/service/StoreDataProvider.java index bb36bc3..aa526b1 100644 --- a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/service/StoreDataProvider.java +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/service/StoreDataProvider.java @@ -7,12 +7,12 @@ import com.won.smarketing.recommend.domain.model.StoreData; * 외부 매장 서비스로부터 매장 정보 조회 기능 정의 */ public interface StoreDataProvider { - + /** - * 매장 ID로 매장 데이터 조회 - * + * 매장 정보 조회 + * * @param storeId 매장 ID * @return 매장 데이터 */ StoreData getStoreData(Long storeId); -} +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/service/WeatherDataProvider.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/service/WeatherDataProvider.java index 5129f46..6f31ae0 100644 --- a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/service/WeatherDataProvider.java +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/domain/service/WeatherDataProvider.java @@ -7,12 +7,12 @@ import com.won.smarketing.recommend.domain.model.WeatherData; * 외부 날씨 API로부터 날씨 정보 조회 기능 정의 */ public interface WeatherDataProvider { - + /** * 특정 위치의 현재 날씨 정보 조회 - * + * * @param location 위치 (주소) * @return 날씨 데이터 */ WeatherData getCurrentWeather(String location); -} +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/AiApiServiceImpl.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/AiApiServiceImpl.java new file mode 100644 index 0000000..b5fbed3 --- /dev/null +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/AiApiServiceImpl.java @@ -0,0 +1,137 @@ +//import com.won.smarketing.recommend.domain.model.StoreData; +//import com.won.smarketing.recommend.domain.model.WeatherData; +//import com.won.smarketing.recommend.domain.service.AiTipGenerator; +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.springframework.beans.factory.annotation.Value; +//import org.springframework.stereotype.Service; +//import org.springframework.web.reactive.function.client.WebClient; +// +//import java.time.Duration; +//import java.util.Map; +// +///** +// * Python AI 팁 생성 구현체 +// */ +//@Slf4j +//@Service +//@RequiredArgsConstructor +//public class PythonAiTipGenerator implements AiTipGenerator { +// +// private final WebClient webClient; +// +// @Value("${external.python-ai-service.base-url}") +// private String pythonAiServiceBaseUrl; +// +// @Value("${external.python-ai-service.api-key}") +// private String pythonAiServiceApiKey; +// +// @Value("${external.python-ai-service.timeout}") +// private int timeout; +// +// @Override +// public String generateTip(StoreData storeData, WeatherData weatherData, String additionalRequirement) { +// try { +// log.debug("Python AI 서비스 호출: store={}, weather={}도", +// storeData.getStoreName(), weatherData.getTemperature()); +// +// // Python AI 서비스 사용 가능 여부 확인 +// if (isPythonServiceAvailable()) { +// return callPythonAiService(storeData, weatherData, additionalRequirement); +// } else { +// log.warn("Python AI 서비스 사용 불가, Fallback 처리"); +// return createFallbackTip(storeData, weatherData, additionalRequirement); +// } +// +// } catch (Exception e) { +// log.error("Python AI 서비스 호출 실패, Fallback 처리: {}", e.getMessage()); +// return createFallbackTip(storeData, weatherData, additionalRequirement); +// } +// } +// +// private boolean isPythonServiceAvailable() { +// return !pythonAiServiceApiKey.equals("dummy-key"); +// } +// +// private String callPythonAiService(StoreData storeData, WeatherData weatherData, String additionalRequirement) { +// try { +// Map requestData = Map.of( +// "store_name", storeData.getStoreName(), +// "business_type", storeData.getBusinessType(), +// "location", storeData.getLocation(), +// "temperature", weatherData.getTemperature(), +// "weather_condition", weatherData.getCondition(), +// "humidity", weatherData.getHumidity(), +// "additional_requirement", additionalRequirement != null ? additionalRequirement : "" +// ); +// +// PythonAiResponse response = webClient +// .post() +// .uri(pythonAiServiceBaseUrl + "/api/v1/generate-marketing-tip") +// .header("Authorization", "Bearer " + pythonAiServiceApiKey) +// .header("Content-Type", "application/json") +// .bodyValue(requestData) +// .retrieve() +// .bodyToMono(PythonAiResponse.class) +// .timeout(Duration.ofMillis(timeout)) +// .block(); +// +// if (response != null && response.getTip() != null && !response.getTip().trim().isEmpty()) { +// return response.getTip(); +// } +// } catch (Exception e) { +// log.error("Python AI 서비스 실제 호출 실패: {}", e.getMessage()); +// } +// +// return createFallbackTip(storeData, weatherData, additionalRequirement); +// } +// +// private String createFallbackTip(StoreData storeData, WeatherData weatherData, String additionalRequirement) { +// String businessType = storeData.getBusinessType(); +// double temperature = weatherData.getTemperature(); +// String condition = weatherData.getCondition(); +// String storeName = storeData.getStoreName(); +// +// // 추가 요청사항이 있는 경우 우선 반영 +// if (additionalRequirement != null && !additionalRequirement.trim().isEmpty()) { +// return String.format("%s에서 %s를 고려한 특별한 서비스로 고객들을 맞이해보세요!", +// storeName, additionalRequirement); +// } +// +// // 날씨와 업종 기반 규칙 +// if (temperature > 25) { +// if (businessType.contains("카페")) { +// return String.format("더운 날씨(%.1f도)에는 시원한 아이스 음료와 디저트로 고객들을 시원하게 만족시켜보세요!", temperature); +// } else { +// return "더운 여름날, 시원한 음료나 냉면으로 고객들에게 청량감을 선사해보세요!"; +// } +// } else if (temperature < 10) { +// if (businessType.contains("카페")) { +// return String.format("추운 날씨(%.1f도)에는 따뜻한 음료와 베이커리로 고객들에게 따뜻함을 전해보세요!", temperature); +// } else { +// return "추운 겨울날, 따뜻한 국물 요리로 고객들의 몸과 마음을 따뜻하게 해보세요!"; +// } +// } +// +// if (condition.contains("비")) { +// return "비 오는 날에는 따뜻한 음료와 분위기로 고객들의 마음을 따뜻하게 해보세요!"; +// } +// +// // 기본 팁 +// return String.format("%s에서 오늘(%.1f도, %s) 같은 날씨에 어울리는 특별한 서비스로 고객들을 맞이해보세요!", +// storeName, temperature, condition); +// } +// +// private static class PythonAiResponse { +// private String tip; +// private String status; +// private String message; +// +// public String getTip() { return tip; } +// public void setTip(String tip) { this.tip = tip; } +// public String getStatus() { return status; } +// public void setStatus(String status) { this.status = status; } +// public String getMessage() { return message; } +// public void setMessage(String message) { this.message = message; } +// } +//} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/ClaudeAiTipGenerator.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/ClaudeAiTipGenerator.java index 2a3f5ce..827ef54 100644 --- a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/ClaudeAiTipGenerator.java +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/ClaudeAiTipGenerator.java @@ -151,7 +151,7 @@ public class ClaudeAiTipGenerator implements AiTipGenerator { } // 업종별 기본 팁 - String businessCategory = storeData.getBusinessCategory(); + String businessCategory = storeData.getBusinessType(); switch (businessCategory) { case "카페": tip.append("인스타그램용 예쁜 음료 사진을 올려보세요."); diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/PythonAiTipGenerator.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/PythonAiTipGenerator.java new file mode 100644 index 0000000..44a5f06 --- /dev/null +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/PythonAiTipGenerator.java @@ -0,0 +1,137 @@ +import com.won.smarketing.recommend.domain.model.StoreData; +import com.won.smarketing.recommend.domain.model.WeatherData; +import com.won.smarketing.recommend.domain.service.AiTipGenerator; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +import java.time.Duration; +import java.util.Map; + +/** + * Python AI 팁 생성 구현체 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PythonAiTipGenerator implements AiTipGenerator { + + private final WebClient webClient; + + @Value("${external.python-ai-service.base-url}") + private String pythonAiServiceBaseUrl; + + @Value("${external.python-ai-service.api-key}") + private String pythonAiServiceApiKey; + + @Value("${external.python-ai-service.timeout}") + private int timeout; + + @Override + public String generateTip(StoreData storeData, WeatherData weatherData, String additionalRequirement) { + try { + log.debug("Python AI 서비스 호출: store={}, weather={}도", + storeData.getStoreName(), weatherData.getTemperature()); + + // Python AI 서비스 사용 가능 여부 확인 + if (isPythonServiceAvailable()) { + return callPythonAiService(storeData, weatherData, additionalRequirement); + } else { + log.warn("Python AI 서비스 사용 불가, Fallback 처리"); + return createFallbackTip(storeData, weatherData, additionalRequirement); + } + + } catch (Exception e) { + log.error("Python AI 서비스 호출 실패, Fallback 처리: {}", e.getMessage()); + return createFallbackTip(storeData, weatherData, additionalRequirement); + } + } + + private boolean isPythonServiceAvailable() { + return !pythonAiServiceApiKey.equals("dummy-key"); + } + + private String callPythonAiService(StoreData storeData, WeatherData weatherData, String additionalRequirement) { + try { + Map requestData = Map.of( + "store_name", storeData.getStoreName(), + "business_type", storeData.getBusinessType(), + "location", storeData.getLocation(), + "temperature", weatherData.getTemperature(), + "weather_condition", weatherData.getCondition(), + "humidity", weatherData.getHumidity(), + "additional_requirement", additionalRequirement != null ? additionalRequirement : "" + ); + + PythonAiResponse response = webClient + .post() + .uri(pythonAiServiceBaseUrl + "/api/v1/generate-marketing-tip") + .header("Authorization", "Bearer " + pythonAiServiceApiKey) + .header("Content-Type", "application/json") + .bodyValue(requestData) + .retrieve() + .bodyToMono(PythonAiResponse.class) + .timeout(Duration.ofMillis(timeout)) + .block(); + + if (response != null && response.getTip() != null && !response.getTip().trim().isEmpty()) { + return response.getTip(); + } + } catch (Exception e) { + log.error("Python AI 서비스 실제 호출 실패: {}", e.getMessage()); + } + + return createFallbackTip(storeData, weatherData, additionalRequirement); + } + + private String createFallbackTip(StoreData storeData, WeatherData weatherData, String additionalRequirement) { + String businessType = storeData.getBusinessType(); + double temperature = weatherData.getTemperature(); + String condition = weatherData.getCondition(); + String storeName = storeData.getStoreName(); + + // 추가 요청사항이 있는 경우 우선 반영 + if (additionalRequirement != null && !additionalRequirement.trim().isEmpty()) { + return String.format("%s에서 %s를 고려한 특별한 서비스로 고객들을 맞이해보세요!", + storeName, additionalRequirement); + } + + // 날씨와 업종 기반 규칙 + if (temperature > 25) { + if (businessType.contains("카페")) { + return String.format("더운 날씨(%.1f도)에는 시원한 아이스 음료와 디저트로 고객들을 시원하게 만족시켜보세요!", temperature); + } else { + return "더운 여름날, 시원한 음료나 냉면으로 고객들에게 청량감을 선사해보세요!"; + } + } else if (temperature < 10) { + if (businessType.contains("카페")) { + return String.format("추운 날씨(%.1f도)에는 따뜻한 음료와 베이커리로 고객들에게 따뜻함을 전해보세요!", temperature); + } else { + return "추운 겨울날, 따뜻한 국물 요리로 고객들의 몸과 마음을 따뜻하게 해보세요!"; + } + } + + if (condition.contains("비")) { + return "비 오는 날에는 따뜻한 음료와 분위기로 고객들의 마음을 따뜻하게 해보세요!"; + } + + // 기본 팁 + return String.format("%s에서 오늘(%.1f도, %s) 같은 날씨에 어울리는 특별한 서비스로 고객들을 맞이해보세요!", + storeName, temperature, condition); + } + + private static class PythonAiResponse { + private String tip; + private String status; + private String message; + + public String getTip() { return tip; } + public void setTip(String tip) { this.tip = tip; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } + } +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/StoreApiDataProvider.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/StoreApiDataProvider.java index 51efb70..ac84ee4 100644 --- a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/StoreApiDataProvider.java +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/StoreApiDataProvider.java @@ -7,16 +7,15 @@ import com.won.smarketing.recommend.domain.service.StoreDataProvider; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientResponseException; -import reactor.core.publisher.Mono; import java.time.Duration; /** * 매장 API 데이터 제공자 구현체 - * 외부 매장 서비스 API를 통해 매장 정보 조회 */ @Slf4j @Service @@ -28,83 +27,98 @@ public class StoreApiDataProvider implements StoreDataProvider { @Value("${external.store-service.base-url}") private String storeServiceBaseUrl; - /** - * 매장 ID로 매장 데이터 조회 - * - * @param storeId 매장 ID - * @return 매장 데이터 - */ + @Value("${external.store-service.timeout}") + private int timeout; + @Override + @Cacheable(value = "storeData", key = "#storeId") public StoreData getStoreData(Long storeId) { try { - log.debug("매장 정보 조회 시작: storeId={}", storeId); - - StoreApiResponse response = webClient - .get() - .uri(storeServiceBaseUrl + "/api/store?storeId=" + storeId) - .retrieve() - .bodyToMono(StoreApiResponse.class) - .timeout(Duration.ofSeconds(10)) - .block(); + log.debug("매장 정보 조회 시도: storeId={}", storeId); - if (response == null || response.getData() == null) { - throw new BusinessException(ErrorCode.STORE_NOT_FOUND); + // 외부 서비스 연결 시도, 실패 시 Mock 데이터 반환 + if (isStoreServiceAvailable()) { + return callStoreService(storeId); + } else { + log.warn("매장 서비스 연결 불가, Mock 데이터 반환: storeId={}", storeId); + return createMockStoreData(storeId); } - StoreApiData storeApiData = response.getData(); - - StoreData storeData = StoreData.builder() - .storeName(storeApiData.getStoreName()) - .businessType(storeApiData.getBusinessType()) - .location(storeApiData.getAddress()) - .build(); - - log.debug("매장 정보 조회 완료: {}", storeData.getStoreName()); - return storeData; - - } catch (WebClientResponseException e) { - log.error("매장 서비스 API 호출 실패: storeId={}, status={}", storeId, e.getStatusCode(), e); - throw new BusinessException(ErrorCode.EXTERNAL_API_ERROR); } catch (Exception e) { - log.error("매장 정보 조회 중 오류 발생: storeId={}", storeId, e); - throw new BusinessException(ErrorCode.EXTERNAL_API_ERROR); + log.error("매장 정보 조회 실패, Mock 데이터 반환: storeId={}", storeId, e); + return createMockStoreData(storeId); } } - /** - * 매장 API 응답 DTO - */ + private boolean isStoreServiceAvailable() { + return !storeServiceBaseUrl.equals("http://localhost:8082"); + } + + private StoreData callStoreService(Long storeId) { + try { + StoreApiResponse response = webClient + .get() + .uri(storeServiceBaseUrl + "/api/store/" + storeId) + .retrieve() + .bodyToMono(StoreApiResponse.class) + .timeout(Duration.ofMillis(timeout)) + .block(); + + if (response != null && response.getData() != null) { + StoreApiResponse.StoreInfo storeInfo = response.getData(); + return StoreData.builder() + .storeName(storeInfo.getStoreName()) + .businessType(storeInfo.getBusinessType()) + .location(storeInfo.getAddress()) + .build(); + } + } catch (WebClientResponseException e) { + if (e.getStatusCode().value() == 404) { + throw new BusinessException(ErrorCode.STORE_NOT_FOUND); + } + log.error("매장 서비스 호출 실패: {}", e.getMessage()); + } + + return createMockStoreData(storeId); + } + + private StoreData createMockStoreData(Long storeId) { + return StoreData.builder() + .storeName("테스트 카페 " + storeId) + .businessType("카페") + .location("서울시 강남구") + .build(); + } + private static class StoreApiResponse { private int status; private String message; - private StoreApiData data; + private StoreInfo data; - // Getters and Setters public int getStatus() { return status; } public void setStatus(int status) { this.status = status; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } - public StoreApiData getData() { return data; } - public void setData(StoreApiData data) { this.data = data; } - } + public StoreInfo getData() { return data; } + public void setData(StoreInfo data) { this.data = data; } - /** - * 매장 API 데이터 DTO - */ - private static class StoreApiData { - private Long storeId; - private String storeName; - private String businessType; - private String address; + static class StoreInfo { + private Long storeId; + private String storeName; + private String businessType; + private String address; + private String phoneNumber; - // Getters and Setters - public Long getStoreId() { return storeId; } - public void setStoreId(Long storeId) { this.storeId = storeId; } - public String getStoreName() { return storeName; } - public void setStoreName(String storeName) { this.storeName = storeName; } - public String getBusinessType() { return businessType; } - public void setBusinessType(String businessType) { this.businessType = businessType; } - public String getAddress() { return address; } - public void setAddress(String address) { this.address = address; } + public Long getStoreId() { return storeId; } + public void setStoreId(Long storeId) { this.storeId = storeId; } + public String getStoreName() { return storeName; } + public void setStoreName(String storeName) { this.storeName = storeName; } + public String getBusinessType() { return businessType; } + public void setBusinessType(String businessType) { this.businessType = businessType; } + public String getAddress() { return address; } + public void setAddress(String address) { this.address = address; } + public String getPhoneNumber() { return phoneNumber; } + public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } + } } -} +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/WeatherApiDataProvider.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/WeatherApiDataProvider.java index 4896c5a..8bf4d7c 100644 --- a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/WeatherApiDataProvider.java +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/external/WeatherApiDataProvider.java @@ -1,22 +1,18 @@ package com.won.smarketing.recommend.infrastructure.external; -import com.won.smarketing.common.exception.BusinessException; -import com.won.smarketing.common.exception.ErrorCode; import com.won.smarketing.recommend.domain.model.WeatherData; import com.won.smarketing.recommend.domain.service.WeatherDataProvider; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.reactive.function.client.WebClientResponseException; -import reactor.core.publisher.Mono; import java.time.Duration; /** * 날씨 API 데이터 제공자 구현체 - * 외부 날씨 API를 통해 날씨 정보 조회 */ @Slf4j @Service @@ -28,128 +24,45 @@ public class WeatherApiDataProvider implements WeatherDataProvider { @Value("${external.weather-api.api-key}") private String weatherApiKey; - @Value("${external.weather-api.base-url}") - private String weatherApiBaseUrl; + @Value("${external.weather-api.timeout}") + private int timeout; - /** - * 특정 위치의 현재 날씨 정보 조회 - * - * @param location 위치 (주소) - * @return 날씨 데이터 - */ @Override - public WeatherApiResponse getCurrentWeather(String location) { + @Cacheable(value = "weatherData", key = "#location") + public WeatherData getCurrentWeather(String location) { try { - log.debug("날씨 정보 조회 시작: location={}", location); - - // 한국 주요 도시로 단순화 - String city = extractCity(location); - - WeatherApiResponse response = webClient - .get() - .uri(uriBuilder -> uriBuilder - .scheme("https") - .host("api.openweathermap.org") - .path("/data/2.5/weather") - .queryParam("q", city + ",KR") - .queryParam("appid", weatherApiKey) - .queryParam("units", "metric") - .queryParam("lang", "kr") - .build()) - .retrieve() - .bodyToMono(WeatherApiResponse.class) - .timeout(Duration.ofSeconds(10)) - .onErrorReturn(createDefaultWeatherData()) // 오류 시 기본값 반환 - .block(); + log.debug("날씨 정보 조회: location={}", location); - if (response == null) { - return createDefaultWeatherData(); + // 개발 환경에서는 Mock 데이터 반환 + if (weatherApiKey.equals("dummy-key")) { + return createMockWeatherData(location); } - WeatherData weatherData = WeatherData.builder() - .temperature(response.getMain().getTemp()) - .condition(response.getWeather()[0].getDescription()) - .humidity(response.getMain().getHumidity()) - .build(); - - log.debug("날씨 정보 조회 완료: {}도, {}", weatherData.getTemperature(), weatherData.getCondition()); - return weatherData; + // 실제 날씨 API 호출 (향후 구현) + return callWeatherApi(location); } catch (Exception e) { - log.warn("날씨 정보 조회 실패, 기본값 사용: location={}", location, e); - return createDefaultWeatherData(); + log.warn("날씨 정보 조회 실패, Mock 데이터 사용: location={}", location, e); + return createMockWeatherData(location); } } - /** - * 주소에서 도시명 추출 - * - * @param location 전체 주소 - * @return 도시명 - */ - private String extractCity(String location) { - if (location == null || location.trim().isEmpty()) { - return "Seoul"; - } - - // 서울, 부산, 대구, 인천, 광주, 대전, 울산 등 주요 도시 추출 - String[] cities = {"서울", "부산", "대구", "인천", "광주", "대전", "울산", "세종", "수원", "창원"}; - - for (String city : cities) { - if (location.contains(city)) { - return city; - } - } - - return "Seoul"; // 기본값 + private WeatherData callWeatherApi(String location) { + // 실제 OpenWeatherMap API 호출 로직 (향후 구현) + log.info("실제 날씨 API 호출: {}", location); + return createMockWeatherData(location); } - /** - * 기본 날씨 데이터 생성 (API 호출 실패 시 사용) - * - * @return 기본 날씨 데이터 - */ - private WeatherApiResponse createDefaultWeatherData() { - WeatherApiResponse response = new WeatherApiResponse(); - response.setMain(new WeatherApiResponse.Main()); - response.getMain().setTemp(20.0); // 기본 온도 20도 - response.getMain().setHumidity(60.0); // 기본 습도 60% - - WeatherApiResponse.Weather[] weather = new WeatherApiResponse.Weather[1]; - weather[0] = new WeatherApiResponse.Weather(); - weather[0].setDescription("맑음"); - response.setWeather(weather); - - return response; + private WeatherData createMockWeatherData(String location) { + double temperature = 20.0 + (Math.random() * 15); // 20-35도 + String[] conditions = {"맑음", "흐림", "비", "눈", "안개"}; + String condition = conditions[(int) (Math.random() * conditions.length)]; + double humidity = 50.0 + (Math.random() * 30); // 50-80% + + return WeatherData.builder() + .temperature(Math.round(temperature * 10) / 10.0) + .condition(condition) + .humidity(Math.round(humidity * 10) / 10.0) + .build(); } - - /** - * 날씨 API 응답 DTO - */ - private static class WeatherApiResponse { - private Main main; - private Weather[] weather; - - public Main getMain() { return main; } - public void setMain(Main main) { this.main = main; } - public Weather[] getWeather() { return weather; } - public void setWeather(Weather[] weather) { this.weather = weather; } - - static class Main { - private Double temp; - private Double humidity; - - public Double getTemp() { return temp; } - public void setTemp(Double temp) { this.temp = temp; } - public Double getHumidity() { return humidity; } - public void setHumidity(Double humidity) { this.humidity = humidity; } - } - - static class Weather { - private String description; - - public String getDescription() { return description; } - public void setDescription(String description) { this.description = description; } - } - } -} +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/persistence/JpaMarketingTipRepository.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/persistence/JpaMarketingTipRepository.java new file mode 100644 index 0000000..45d7218 --- /dev/null +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/persistence/JpaMarketingTipRepository.java @@ -0,0 +1,38 @@ +import com.won.smarketing.recommend.domain.model.MarketingTip; +import com.won.smarketing.recommend.domain.repository.MarketingTipRepository; +import com.won.smarketing.recommend.infrastructure.persistence.MarketingTipJpaRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * JPA 마케팅 팁 레포지토리 구현체 + */ +@Repository +@RequiredArgsConstructor +public class JpaMarketingTipRepository implements MarketingTipRepository { + + private final MarketingTipJpaRepository jpaRepository; + + @Override + public MarketingTip save(MarketingTip marketingTip) { + com.won.smarketing.recommend.entity.MarketingTipEntity entity = MarketingTipEntity.fromDomain(marketingTip); + com.won.smarketing.recommend.entity.MarketingTipEntity savedEntity = jpaRepository.save(entity); + return savedEntity.toDomain(); + } + + @Override + public Optional findById(Long tipId) { + return jpaRepository.findById(tipId) + .map(MarketingTipEntity::toDomain); + } + + @Override + public Page findByStoreIdOrderByCreatedAtDesc(Long storeId, Pageable pageable) { + return jpaRepository.findByStoreIdOrderByCreatedAtDesc(storeId, pageable) + .map(MarketingTipEntity::toDomain); + } +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/persistence/MarketingTipEntity.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/persistence/MarketingTipEntity.java new file mode 100644 index 0000000..7d47714 --- /dev/null +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/persistence/MarketingTipEntity.java @@ -0,0 +1,58 @@ +package com.won.smarketing.recommend.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +/** + * 마케팅 팁 JPA 엔티티 + */ +@Entity +@Table(name = "marketing_tips") +@EntityListeners(AuditingEntityListener.class) +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MarketingTipEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "store_id", nullable = false) + private Long storeId; + + @Column(name = "tip_content", columnDefinition = "TEXT", nullable = false) + private String tipContent; + + // WeatherData 임베디드 + @Column(name = "weather_temperature") + private Double weatherTemperature; + + @Column(name = "weather_condition", length = 100) + private String weatherCondition; + + @Column(name = "weather_humidity") + private Double weatherHumidity; + + // StoreData 임베디드 + @Column(name = "store_name", length = 200) + private String storeName; + + @Column(name = "business_type", length = 100) + private String businessType; + + @Column(name = "store_location", length = 500) + private String storeLocation; + + @CreatedDate + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/persistence/MarketingTipJpaRepository.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/persistence/MarketingTipJpaRepository.java new file mode 100644 index 0000000..ca1ec19 --- /dev/null +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/infrastructure/persistence/MarketingTipJpaRepository.java @@ -0,0 +1,14 @@ +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +/** + * 마케팅 팁 JPA 레포지토리 + */ +public interface MarketingTipJpaRepository extends JpaRepository { + + @Query("SELECT m FROM MarketingTipEntity m WHERE m.storeId = :storeId ORDER BY m.createdAt DESC") + Page findByStoreIdOrderByCreatedAtDesc(@Param("storeId") Long storeId, Pageable pageable); +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/controller/RecommendationController.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/controller/RecommendationController.java index e929efb..fbbab48 100644 --- a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/controller/RecommendationController.java +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/controller/RecommendationController.java @@ -5,35 +5,73 @@ import com.won.smarketing.recommend.application.usecase.MarketingTipUseCase; import com.won.smarketing.recommend.presentation.dto.MarketingTipRequest; import com.won.smarketing.recommend.presentation.dto.MarketingTipResponse; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import jakarta.validation.Valid; /** - * AI 마케팅 추천을 위한 REST API 컨트롤러 - * AI 기반 마케팅 팁 생성 기능 제공 + * AI 마케팅 추천 컨트롤러 */ -@Tag(name = "AI 마케팅 추천", description = "AI 기반 맞춤형 마케팅 추천 API") +@Tag(name = "AI 추천", description = "AI 기반 마케팅 팁 추천 API") +@Slf4j @RestController -@RequestMapping("/api/recommendation") +@RequestMapping("/api/recommendations") @RequiredArgsConstructor public class RecommendationController { private final MarketingTipUseCase marketingTipUseCase; - /** - * AI 마케팅 팁 생성 - * - * @param request 마케팅 팁 생성 요청 - * @return 생성된 마케팅 팁 - */ - @Operation(summary = "AI 마케팅 팁 생성", description = "매장 특성과 환경 정보를 바탕으로 AI 마케팅 팁을 생성합니다.") + @Operation( + summary = "AI 마케팅 팁 생성", + description = "매장 정보와 환경 데이터를 기반으로 AI 마케팅 팁을 생성합니다." + ) @PostMapping("/marketing-tips") - public ResponseEntity> generateMarketingTips(@Valid @RequestBody MarketingTipRequest request) { + public ResponseEntity> generateMarketingTips( + @Parameter(description = "마케팅 팁 생성 요청") @Valid @RequestBody MarketingTipRequest request) { + + log.info("AI 마케팅 팁 생성 요청: storeId={}", request.getStoreId()); + MarketingTipResponse response = marketingTipUseCase.generateMarketingTips(request); - return ResponseEntity.ok(ApiResponse.success(response, "AI 마케팅 팁이 성공적으로 생성되었습니다.")); + + log.info("AI 마케팅 팁 생성 완료: tipId={}", response.getTipId()); + return ResponseEntity.ok(ApiResponse.success(response)); } -} + + @Operation( + summary = "마케팅 팁 이력 조회", + description = "특정 매장의 마케팅 팁 생성 이력을 조회합니다." + ) + @GetMapping("/marketing-tips") + public ResponseEntity>> getMarketingTipHistory( + @Parameter(description = "매장 ID") @RequestParam Long storeId, + Pageable pageable) { + + log.info("마케팅 팁 이력 조회: storeId={}, page={}", storeId, pageable.getPageNumber()); + + Page response = marketingTipUseCase.getMarketingTipHistory(storeId, pageable); + + return ResponseEntity.ok(ApiResponse.success(response)); + } + + @Operation( + summary = "마케팅 팁 상세 조회", + description = "특정 마케팅 팁의 상세 정보를 조회합니다." + ) + @GetMapping("/marketing-tips/{tipId}") + public ResponseEntity> getMarketingTip( + @Parameter(description = "팁 ID") @PathVariable Long tipId) { + + log.info("마케팅 팁 상세 조회: tipId={}", tipId); + + MarketingTipResponse response = marketingTipUseCase.getMarketingTip(tipId); + + return ResponseEntity.ok(ApiResponse.success(response)); + } +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/dto/AIServiceRequest.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/dto/AIServiceRequest.java new file mode 100644 index 0000000..396e20c --- /dev/null +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/dto/AIServiceRequest.java @@ -0,0 +1,24 @@ +package com.won.smarketing.recommend.presentation.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * Python AI 서비스 요청 DTO + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AIServiceRequest { + + private String serviceType; // "marketing_tips", "business_insights", "trend_analysis" + private Long storeId; + private String category; + private Map parameters; + private Map context; // 매장 정보, 과거 데이터 등 +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/dto/MarketingTipRequest.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/dto/MarketingTipRequest.java index 0bf5ff8..c706619 100644 --- a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/dto/MarketingTipRequest.java +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/dto/MarketingTipRequest.java @@ -1,24 +1,26 @@ package com.won.smarketing.recommend.presentation.dto; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Positive; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -/** - * AI 마케팅 팁 생성 요청 DTO - * 매장 정보를 기반으로 개인화된 마케팅 팁을 요청할 때 사용됩니다. - */ +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; + +@Schema(description = "마케팅 팁 생성 요청") @Data +@Builder @NoArgsConstructor @AllArgsConstructor -@Schema(description = "AI 마케팅 팁 생성 요청") public class MarketingTipRequest { - + + @Schema(description = "매장 ID", example = "1", required = true) @NotNull(message = "매장 ID는 필수입니다") @Positive(message = "매장 ID는 양수여야 합니다") - @Schema(description = "매장 ID", example = "1", required = true) private Long storeId; -} + + @Schema(description = "추가 요청사항", example = "여름철 음료 프로모션에 집중해주세요") + private String additionalRequirement; +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/dto/MarketingTipResponse.java b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/dto/MarketingTipResponse.java index ca1ffe0..047f34b 100644 --- a/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/dto/MarketingTipResponse.java +++ b/smarketing-java/ai-recommend/src/main/java/com/won/smarketing/recommend/presentation/dto/MarketingTipResponse.java @@ -8,24 +8,61 @@ import lombok.NoArgsConstructor; import java.time.LocalDateTime; -/** - * AI 마케팅 팁 생성 응답 DTO - * AI가 생성한 개인화된 마케팅 팁 정보를 전달합니다. - */ +@Schema(description = "마케팅 팁 응답") @Data +@Builder @NoArgsConstructor @AllArgsConstructor -@Builder -@Schema(description = "AI 마케팅 팁 생성 응답") public class MarketingTipResponse { - + @Schema(description = "팁 ID", example = "1") private Long tipId; - - @Schema(description = "AI 생성 마케팅 팁 내용 (100자 이내)", - example = "오늘 같은 비 오는 날에는 따뜻한 음료와 함께 실내 분위기를 강조한 포스팅을 올려보세요. #비오는날카페 #따뜻한음료 해시태그로 감성을 어필해보세요!") + + @Schema(description = "매장 ID", example = "1") + private Long storeId; + + @Schema(description = "매장명", example = "카페 봄날") + private String storeName; + + @Schema(description = "AI 생성 마케팅 팁 내용") private String tipContent; - - @Schema(description = "팁 생성 시간", example = "2024-01-15T10:30:00") + + @Schema(description = "날씨 정보") + private WeatherInfo weatherInfo; + + @Schema(description = "매장 정보") + private StoreInfo storeInfo; + + @Schema(description = "생성 일시") private LocalDateTime createdAt; -} + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class WeatherInfo { + @Schema(description = "온도", example = "25.5") + private Double temperature; + + @Schema(description = "날씨 상태", example = "맑음") + private String condition; + + @Schema(description = "습도", example = "60.0") + private Double humidity; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class StoreInfo { + @Schema(description = "매장명", example = "카페 봄날") + private String storeName; + + @Schema(description = "업종", example = "카페") + private String businessType; + + @Schema(description = "위치", example = "서울시 강남구") + private String location; + } +} \ No newline at end of file diff --git a/smarketing-java/ai-recommend/src/main/resources/application.yml b/smarketing-java/ai-recommend/src/main/resources/application.yml index 8a6cb92..c3caad4 100644 --- a/smarketing-java/ai-recommend/src/main/resources/application.yml +++ b/smarketing-java/ai-recommend/src/main/resources/application.yml @@ -7,7 +7,7 @@ spring: application: name: ai-recommend-service datasource: - url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:recommenddb} + url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:AiRecommendationDB} username: ${POSTGRES_USER:postgres} password: ${POSTGRES_PASSWORD:postgres} jpa: @@ -18,6 +18,11 @@ spring: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect format_sql: true + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASSWORD:} ai: service: @@ -47,4 +52,8 @@ springdoc: logging: level: com.won.smarketing.recommend: ${LOG_LEVEL:DEBUG} - \ No newline at end of file + +jwt: + secret: ${JWT_SECRET:mySecretKeyForJWTTokenGenerationAndValidation123456789} + access-token-validity: ${JWT_ACCESS_VALIDITY:3600000} + refresh-token-validity: ${JWT_REFRESH_VALIDITY:604800000}