mirror of
https://github.com/won-ktds/smarketing-backend.git
synced 2025-12-06 07:06:24 +00:00
fix: ai-recommend fix
This commit is contained in:
parent
2004d7c736
commit
66a4faf3ac
@ -4,12 +4,14 @@ import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
|
||||
/**
|
||||
* AI 추천 서비스 메인 애플리케이션
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@SpringBootApplication(scanBasePackages = {
|
||||
"com.won.smarketing.recommend",
|
||||
"com.won.smarketing.common"
|
||||
})
|
||||
@EnableJpaAuditing
|
||||
@EnableJpaRepositories(basePackages = "com.won.smarketing.recommend.infrastructure.persistence")
|
||||
@EnableCaching
|
||||
public class AIRecommendServiceApplication {
|
||||
public static void main(String[] args) {
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@ -5,11 +5,8 @@ import com.won.smarketing.common.exception.ErrorCode;
|
||||
import com.won.smarketing.recommend.application.usecase.MarketingTipUseCase;
|
||||
import com.won.smarketing.recommend.domain.model.MarketingTip;
|
||||
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.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;
|
||||
@ -32,42 +29,36 @@ public class MarketingTipService implements MarketingTipUseCase {
|
||||
|
||||
private final MarketingTipRepository marketingTipRepository;
|
||||
private final StoreDataProvider storeDataProvider;
|
||||
private final WeatherDataProvider weatherDataProvider;
|
||||
private final AiTipGenerator aiTipGenerator;
|
||||
|
||||
@Override
|
||||
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(), weatherData.getCondition());
|
||||
|
||||
// 3. AI 팁 생성
|
||||
String aiGeneratedTip = aiTipGenerator.generateTip(storeData, weatherData, request.getAdditionalRequirement());
|
||||
|
||||
// 2. Python AI 서비스로 팁 생성 (매장 정보 + 추가 요청사항 전달)
|
||||
String aiGeneratedTip = aiTipGenerator.generateTip(storeData, request.getAdditionalRequirement());
|
||||
log.debug("AI 팁 생성 완료: {}", aiGeneratedTip.substring(0, Math.min(50, aiGeneratedTip.length())));
|
||||
|
||||
// 4. 도메인 객체 생성 및 저장
|
||||
|
||||
// 3. 도메인 객체 생성 및 저장
|
||||
MarketingTip marketingTip = MarketingTip.builder()
|
||||
.storeId(request.getStoreId())
|
||||
.tipContent(aiGeneratedTip)
|
||||
.weatherData(weatherData)
|
||||
.storeData(storeData)
|
||||
.build();
|
||||
|
||||
|
||||
MarketingTip savedTip = marketingTipRepository.save(marketingTip);
|
||||
log.info("마케팅 팁 저장 완료: tipId={}", savedTip.getId().getValue());
|
||||
|
||||
|
||||
return convertToResponse(savedTip);
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("마케팅 팁 생성 중 오류: storeId={}", request.getStoreId(), e);
|
||||
throw new BusinessException(ErrorCode.AI_TIP_GENERATION_FAILED);
|
||||
throw new BusinessException(ErrorCode.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,9 +67,9 @@ public class MarketingTipService implements MarketingTipUseCase {
|
||||
@Cacheable(value = "marketingTipHistory", key = "#storeId + '_' + #pageable.pageNumber + '_' + #pageable.pageSize")
|
||||
public Page<MarketingTipResponse> getMarketingTipHistory(Long storeId, Pageable pageable) {
|
||||
log.info("마케팅 팁 이력 조회: storeId={}", storeId);
|
||||
|
||||
|
||||
Page<MarketingTip> tips = marketingTipRepository.findByStoreIdOrderByCreatedAtDesc(storeId, pageable);
|
||||
|
||||
|
||||
return tips.map(this::convertToResponse);
|
||||
}
|
||||
|
||||
@ -86,10 +77,10 @@ public class MarketingTipService implements MarketingTipUseCase {
|
||||
@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));
|
||||
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.INTERNAL_SERVER_ERROR));
|
||||
|
||||
return convertToResponse(marketingTip);
|
||||
}
|
||||
|
||||
@ -98,32 +89,13 @@ public class MarketingTipService implements MarketingTipUseCase {
|
||||
.tipId(marketingTip.getId().getValue())
|
||||
.storeId(marketingTip.getStoreId())
|
||||
.storeName(marketingTip.getStoreData().getStoreName())
|
||||
.businessType(marketingTip.getStoreData().getBusinessType())
|
||||
.storeLocation(marketingTip.getStoreData().getLocation())
|
||||
.tipContent(marketingTip.getTipContent())
|
||||
.storeInfo(MarketingTipResponse.StoreInfo.builder()
|
||||
.storeName(marketingTip.getStoreData().getStoreName())
|
||||
.businessType(marketingTip.getStoreData().getBusinessType())
|
||||
.location(marketingTip.getStoreData().getLocation())
|
||||
.build())
|
||||
.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -6,33 +6,22 @@ import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
/**
|
||||
* 마케팅 팁 생성 유즈케이스 인터페이스
|
||||
* 비즈니스 요구사항을 정의하는 애플리케이션 계층의 인터페이스
|
||||
* 마케팅 팁 유즈케이스 인터페이스
|
||||
*/
|
||||
public interface MarketingTipUseCase {
|
||||
|
||||
|
||||
/**
|
||||
* AI 마케팅 팁 생성
|
||||
*
|
||||
* @param request 마케팅 팁 생성 요청
|
||||
* @return 생성된 마케팅 팁 정보
|
||||
*/
|
||||
MarketingTipResponse generateMarketingTips(MarketingTipRequest request);
|
||||
|
||||
|
||||
/**
|
||||
* 마케팅 팁 이력 조회
|
||||
*
|
||||
* @param storeId 매장 ID
|
||||
* @param pageable 페이징 정보
|
||||
* @return 마케팅 팁 이력 페이지
|
||||
*/
|
||||
Page<MarketingTipResponse> getMarketingTipHistory(Long storeId, Pageable pageable);
|
||||
|
||||
|
||||
/**
|
||||
* 마케팅 팁 상세 조회
|
||||
*
|
||||
* @param tipId 팁 ID
|
||||
* @return 마케팅 팁 상세 정보
|
||||
*/
|
||||
MarketingTipResponse getMarketingTip(Long tipId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,4 +10,4 @@ import org.springframework.context.annotation.Configuration;
|
||||
@EnableCaching
|
||||
public class CacheConfig {
|
||||
// 기본 Simple 캐시 사용
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,15 +4,13 @@ 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 설정
|
||||
* WebClient 설정 (간소화된 버전)
|
||||
*/
|
||||
@Configuration
|
||||
public class WebClientConfig {
|
||||
@ -21,9 +19,7 @@ public class WebClientConfig {
|
||||
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)));
|
||||
.responseTimeout(Duration.ofMillis(5000));
|
||||
|
||||
return WebClient.builder()
|
||||
.clientConnector(new ReactorClientHttpConnector(httpClient))
|
||||
|
||||
@ -1,51 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,8 +1,5 @@
|
||||
package com.won.smarketing.recommend.domain.model;
|
||||
|
||||
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;
|
||||
@ -11,28 +8,26 @@ import lombok.NoArgsConstructor;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 마케팅 팁 도메인 모델
|
||||
* 마케팅 팁 도메인 모델 (날씨 정보 제거)
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MarketingTip {
|
||||
|
||||
|
||||
private TipId id;
|
||||
private Long storeId;
|
||||
private String tipContent;
|
||||
private WeatherData weatherData;
|
||||
private StoreData storeData;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public static MarketingTip create(Long storeId, String tipContent, WeatherData weatherData, StoreData storeData) {
|
||||
|
||||
public static MarketingTip create(Long storeId, String tipContent, StoreData storeData) {
|
||||
return MarketingTip.builder()
|
||||
.storeId(storeId)
|
||||
.tipContent(tipContent)
|
||||
.weatherData(weatherData)
|
||||
.storeData(storeData)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,4 +16,4 @@ public class StoreData {
|
||||
private String storeName;
|
||||
private String businessType;
|
||||
private String location;
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,8 +14,8 @@ import lombok.NoArgsConstructor;
|
||||
@AllArgsConstructor
|
||||
public class TipId {
|
||||
private Long value;
|
||||
|
||||
|
||||
public static TipId of(Long value) {
|
||||
return new TipId(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
package com.won.smarketing.recommend.domain.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 날씨 데이터 값 객체
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class WeatherData {
|
||||
private Double temperature;
|
||||
private String condition;
|
||||
private Double humidity;
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
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<BusinessInsight, Long> {
|
||||
|
||||
List<BusinessInsight> findByStoreIdOrderByCreatedAtDesc(Long storeId);
|
||||
|
||||
List<BusinessInsight> findByInsightTypeAndStoreId(String insightType, Long storeId);
|
||||
}
|
||||
@ -7,7 +7,7 @@ import org.springframework.data.domain.Pageable;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 마케팅 팁 레포지토리 인터페이스
|
||||
* 마케팅 팁 레포지토리 인터페이스 (순수한 도메인 인터페이스)
|
||||
*/
|
||||
public interface MarketingTipRepository {
|
||||
|
||||
|
||||
@ -1,20 +1,18 @@
|
||||
package com.won.smarketing.recommend.domain.service;
|
||||
|
||||
import com.won.smarketing.recommend.domain.model.StoreData;
|
||||
import com.won.smarketing.recommend.domain.model.WeatherData;
|
||||
|
||||
/**
|
||||
* AI 팁 생성 도메인 서비스 인터페이스
|
||||
* AI를 활용한 마케팅 팁 생성 기능 정의
|
||||
* AI 팁 생성 도메인 서비스 인터페이스 (단순화)
|
||||
*/
|
||||
public interface AiTipGenerator {
|
||||
|
||||
/**
|
||||
* 매장 정보와 날씨 정보를 바탕으로 마케팅 팁 생성
|
||||
* Python AI 서비스를 통한 마케팅 팁 생성
|
||||
*
|
||||
* @param storeData 매장 데이터
|
||||
* @param weatherData 날씨 데이터
|
||||
* @param storeData 매장 정보
|
||||
* @param additionalRequirement 추가 요청사항
|
||||
* @return AI가 생성한 마케팅 팁
|
||||
*/
|
||||
String generateTip(StoreData storeData, WeatherData weatherData);
|
||||
String generateTip(StoreData storeData, String additionalRequirement);
|
||||
}
|
||||
|
||||
@ -4,15 +4,8 @@ import com.won.smarketing.recommend.domain.model.StoreData;
|
||||
|
||||
/**
|
||||
* 매장 데이터 제공 도메인 서비스 인터페이스
|
||||
* 외부 매장 서비스로부터 매장 정보 조회 기능 정의
|
||||
*/
|
||||
public interface StoreDataProvider {
|
||||
|
||||
/**
|
||||
* 매장 정보 조회
|
||||
*
|
||||
* @param storeId 매장 ID
|
||||
* @return 매장 데이터
|
||||
*/
|
||||
|
||||
StoreData getStoreData(Long storeId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
package com.won.smarketing.recommend.domain.service;
|
||||
|
||||
import com.won.smarketing.recommend.domain.model.WeatherData;
|
||||
|
||||
/**
|
||||
* 날씨 데이터 제공 도메인 서비스 인터페이스
|
||||
* 외부 날씨 API로부터 날씨 정보 조회 기능 정의
|
||||
*/
|
||||
public interface WeatherDataProvider {
|
||||
|
||||
/**
|
||||
* 특정 위치의 현재 날씨 정보 조회
|
||||
*
|
||||
* @param location 위치 (주소)
|
||||
* @return 날씨 데이터
|
||||
*/
|
||||
WeatherData getCurrentWeather(String location);
|
||||
}
|
||||
@ -1,137 +0,0 @@
|
||||
//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<String, Object> 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; }
|
||||
// }
|
||||
//}
|
||||
@ -1,190 +0,0 @@
|
||||
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.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.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
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;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Claude AI 팁 생성기 구현체
|
||||
* Claude AI API를 통해 마케팅 팁 생성
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ClaudeAiTipGenerator implements AiTipGenerator {
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
@Value("${external.claude-ai.api-key}")
|
||||
private String claudeApiKey;
|
||||
|
||||
@Value("${external.claude-ai.base-url}")
|
||||
private String claudeApiBaseUrl;
|
||||
|
||||
@Value("${external.claude-ai.model}")
|
||||
private String claudeModel;
|
||||
|
||||
@Value("${external.claude-ai.max-tokens}")
|
||||
private Integer maxTokens;
|
||||
|
||||
/**
|
||||
* 매장 정보와 날씨 정보를 바탕으로 마케팅 팁 생성
|
||||
*
|
||||
* @param storeData 매장 데이터
|
||||
* @param weatherData 날씨 데이터
|
||||
* @return AI가 생성한 마케팅 팁
|
||||
*/
|
||||
@Override
|
||||
public String generateTip(StoreData storeData, WeatherData weatherData) {
|
||||
try {
|
||||
log.debug("AI 마케팅 팁 생성 시작: store={}, weather={}도",
|
||||
storeData.getStoreName(), weatherData.getTemperature());
|
||||
|
||||
String prompt = buildPrompt(storeData, weatherData);
|
||||
|
||||
Map<String, Object> requestBody = Map.of(
|
||||
"model", claudeModel,
|
||||
"max_tokens", maxTokens,
|
||||
"messages", new Object[]{
|
||||
Map.of(
|
||||
"role", "user",
|
||||
"content", prompt
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
ClaudeApiResponse response = webClient
|
||||
.post()
|
||||
.uri(claudeApiBaseUrl + "/v1/messages")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + claudeApiKey)
|
||||
.header("anthropic-version", "2023-06-01")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(requestBody)
|
||||
.retrieve()
|
||||
.bodyToMono(ClaudeApiResponse.class)
|
||||
.timeout(Duration.ofSeconds(30))
|
||||
.block();
|
||||
|
||||
if (response == null || response.getContent() == null || response.getContent().length == 0) {
|
||||
throw new BusinessException(ErrorCode.AI_SERVICE_UNAVAILABLE);
|
||||
}
|
||||
|
||||
String generatedTip = response.getContent()[0].getText();
|
||||
|
||||
// 100자 제한 적용
|
||||
if (generatedTip.length() > 100) {
|
||||
generatedTip = generatedTip.substring(0, 97) + "...";
|
||||
}
|
||||
|
||||
log.debug("AI 마케팅 팁 생성 완료: length={}", generatedTip.length());
|
||||
return generatedTip;
|
||||
|
||||
} catch (WebClientResponseException e) {
|
||||
log.error("Claude AI API 호출 실패: status={}", e.getStatusCode(), e);
|
||||
return generateFallbackTip(storeData, weatherData);
|
||||
} catch (Exception e) {
|
||||
log.error("AI 마케팅 팁 생성 중 오류 발생", e);
|
||||
return generateFallbackTip(storeData, weatherData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 프롬프트 구성
|
||||
*
|
||||
* @param storeData 매장 데이터
|
||||
* @param weatherData 날씨 데이터
|
||||
* @return 프롬프트 문자열
|
||||
*/
|
||||
private String buildPrompt(StoreData storeData, WeatherData weatherData) {
|
||||
return String.format(
|
||||
"다음 매장을 위한 오늘의 마케팅 팁을 100자 이내로 작성해주세요.\n\n" +
|
||||
"매장 정보:\n" +
|
||||
"- 매장명: %s\n" +
|
||||
"- 업종: %s\n" +
|
||||
"- 위치: %s\n\n" +
|
||||
"오늘 날씨:\n" +
|
||||
"- 온도: %.1f도\n" +
|
||||
"- 날씨: %s\n" +
|
||||
"- 습도: %.1f%%\n\n" +
|
||||
"날씨와 매장 특성을 고려한 실용적이고 구체적인 마케팅 팁을 제안해주세요. " +
|
||||
"반드시 100자 이내로 작성하고, 친근하고 실행 가능한 조언을 해주세요.",
|
||||
storeData.getStoreName(),
|
||||
storeData.getBusinessType(),
|
||||
storeData.getLocation(),
|
||||
weatherData.getTemperature(),
|
||||
weatherData.getCondition(),
|
||||
weatherData.getHumidity()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* AI API 실패 시 대체 팁 생성
|
||||
*
|
||||
* @param storeData 매장 데이터
|
||||
* @param weatherData 날씨 데이터
|
||||
* @return 대체 마케팅 팁
|
||||
*/
|
||||
private String generateFallbackTip(StoreData storeData, WeatherData weatherData) {
|
||||
StringBuilder tip = new StringBuilder();
|
||||
|
||||
// 날씨 기반 기본 팁
|
||||
if (weatherData.getTemperature() >= 25) {
|
||||
tip.append("더운 날씨에는 시원한 음료나 디저트를 홍보해보세요! ");
|
||||
} else if (weatherData.getTemperature() <= 10) {
|
||||
tip.append("추운 날씨에는 따뜻한 메뉴를 강조해보세요! ");
|
||||
} else {
|
||||
tip.append("좋은 날씨를 활용한 야외석 이용을 추천해보세요! ");
|
||||
}
|
||||
|
||||
// 업종별 기본 팁
|
||||
String businessCategory = storeData.getBusinessType();
|
||||
switch (businessCategory) {
|
||||
case "카페":
|
||||
tip.append("인스타그램용 예쁜 음료 사진을 올려보세요.");
|
||||
break;
|
||||
case "음식점":
|
||||
tip.append("시그니처 메뉴의 맛있는 사진을 SNS에 공유해보세요.");
|
||||
break;
|
||||
default:
|
||||
tip.append("오늘의 특별 메뉴를 SNS에 홍보해보세요.");
|
||||
break;
|
||||
}
|
||||
|
||||
String fallbackTip = tip.toString();
|
||||
return fallbackTip.length() > 100 ? fallbackTip.substring(0, 97) + "..." : fallbackTip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Claude API 응답 DTO
|
||||
*/
|
||||
private static class ClaudeApiResponse {
|
||||
private Content[] content;
|
||||
|
||||
public Content[] getContent() { return content; }
|
||||
public void setContent(Content[] content) { this.content = content; }
|
||||
|
||||
static class Content {
|
||||
private String text;
|
||||
private String type;
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
public String getType() { return type; }
|
||||
public void setType(String type) { this.type = type; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,20 +1,21 @@
|
||||
package com.won.smarketing.recommend.infrastructure.external;
|
||||
|
||||
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.stereotype.Service; // 이 어노테이션이 누락되어 있었음
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Python AI 팁 생성 구현체
|
||||
* Python AI 팁 생성 구현체 (날씨 정보 제거)
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@Service // 추가된 어노테이션
|
||||
@RequiredArgsConstructor
|
||||
public class PythonAiTipGenerator implements AiTipGenerator {
|
||||
|
||||
@ -30,22 +31,21 @@ public class PythonAiTipGenerator implements AiTipGenerator {
|
||||
private int timeout;
|
||||
|
||||
@Override
|
||||
public String generateTip(StoreData storeData, WeatherData weatherData, String additionalRequirement) {
|
||||
public String generateTip(StoreData storeData, String additionalRequirement) {
|
||||
try {
|
||||
log.debug("Python AI 서비스 호출: store={}, weather={}도",
|
||||
storeData.getStoreName(), weatherData.getTemperature());
|
||||
log.debug("Python AI 서비스 호출: store={}", storeData.getStoreName());
|
||||
|
||||
// Python AI 서비스 사용 가능 여부 확인
|
||||
if (isPythonServiceAvailable()) {
|
||||
return callPythonAiService(storeData, weatherData, additionalRequirement);
|
||||
return callPythonAiService(storeData, additionalRequirement);
|
||||
} else {
|
||||
log.warn("Python AI 서비스 사용 불가, Fallback 처리");
|
||||
return createFallbackTip(storeData, weatherData, additionalRequirement);
|
||||
return createFallbackTip(storeData, additionalRequirement);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Python AI 서비스 호출 실패, Fallback 처리: {}", e.getMessage());
|
||||
return createFallbackTip(storeData, weatherData, additionalRequirement);
|
||||
return createFallbackTip(storeData, additionalRequirement);
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,18 +53,18 @@ public class PythonAiTipGenerator implements AiTipGenerator {
|
||||
return !pythonAiServiceApiKey.equals("dummy-key");
|
||||
}
|
||||
|
||||
private String callPythonAiService(StoreData storeData, WeatherData weatherData, String additionalRequirement) {
|
||||
private String callPythonAiService(StoreData storeData, String additionalRequirement) {
|
||||
try {
|
||||
// Python AI 서비스로 전송할 데이터 (날씨 정보 제거, 매장 정보만 전달)
|
||||
Map<String, Object> 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 : ""
|
||||
);
|
||||
|
||||
log.debug("Python AI 서비스 요청 데이터: {}", requestData);
|
||||
|
||||
PythonAiResponse response = webClient
|
||||
.post()
|
||||
.uri(pythonAiServiceBaseUrl + "/api/v1/generate-marketing-tip")
|
||||
@ -77,49 +77,50 @@ public class PythonAiTipGenerator implements AiTipGenerator {
|
||||
.block();
|
||||
|
||||
if (response != null && response.getTip() != null && !response.getTip().trim().isEmpty()) {
|
||||
log.debug("Python AI 서비스 응답 성공: tip length={}", response.getTip().length());
|
||||
return response.getTip();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Python AI 서비스 실제 호출 실패: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return createFallbackTip(storeData, weatherData, additionalRequirement);
|
||||
return createFallbackTip(storeData, additionalRequirement);
|
||||
}
|
||||
|
||||
private String createFallbackTip(StoreData storeData, WeatherData weatherData, String additionalRequirement) {
|
||||
/**
|
||||
* 규칙 기반 Fallback 팁 생성 (날씨 정보 없이 매장 정보만 활용)
|
||||
*/
|
||||
private String createFallbackTip(StoreData storeData, String additionalRequirement) {
|
||||
String businessType = storeData.getBusinessType();
|
||||
double temperature = weatherData.getTemperature();
|
||||
String condition = weatherData.getCondition();
|
||||
String storeName = storeData.getStoreName();
|
||||
String location = storeData.getLocation();
|
||||
|
||||
// 추가 요청사항이 있는 경우 우선 반영
|
||||
if (additionalRequirement != null && !additionalRequirement.trim().isEmpty()) {
|
||||
return String.format("%s에서 %s를 고려한 특별한 서비스로 고객들을 맞이해보세요!",
|
||||
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 (businessType.contains("카페")) {
|
||||
return String.format("%s만의 시그니처 음료와 디저트로 고객들에게 특별한 경험을 선사해보세요!", storeName);
|
||||
} else if (businessType.contains("음식점") || businessType.contains("식당")) {
|
||||
return String.format("%s의 대표 메뉴를 활용한 특별한 이벤트로 고객들의 관심을 끌어보세요!", storeName);
|
||||
} else if (businessType.contains("베이커리") || businessType.contains("빵집")) {
|
||||
return String.format("%s의 갓 구운 빵과 함께하는 따뜻한 서비스로 고객들의 마음을 사로잡아보세요!", storeName);
|
||||
} else if (businessType.contains("치킨") || businessType.contains("튀김")) {
|
||||
return String.format("%s의 바삭하고 맛있는 메뉴로 고객들에게 만족스러운 식사를 제공해보세요!", storeName);
|
||||
}
|
||||
|
||||
if (condition.contains("비")) {
|
||||
return "비 오는 날에는 따뜻한 음료와 분위기로 고객들의 마음을 따뜻하게 해보세요!";
|
||||
// 지역별 팁
|
||||
if (location.contains("강남") || location.contains("서초")) {
|
||||
return String.format("%s에서 트렌디하고 세련된 서비스로 젊은 고객층을 공략해보세요!", storeName);
|
||||
} else if (location.contains("홍대") || location.contains("신촌")) {
|
||||
return String.format("%s에서 활기차고 개성 있는 이벤트로 대학생들의 관심을 끌어보세요!", storeName);
|
||||
}
|
||||
|
||||
// 기본 팁
|
||||
return String.format("%s에서 오늘(%.1f도, %s) 같은 날씨에 어울리는 특별한 서비스로 고객들을 맞이해보세요!",
|
||||
storeName, temperature, condition);
|
||||
return String.format("%s만의 특별함을 살린 고객 맞춤 서비스로 단골 고객을 늘려보세요!", storeName);
|
||||
}
|
||||
|
||||
private static class PythonAiResponse {
|
||||
|
||||
@ -8,7 +8,7 @@ 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.stereotype.Service; // 이 어노테이션이 누락되어 있었음
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
|
||||
@ -18,7 +18,7 @@ import java.time.Duration;
|
||||
* 매장 API 데이터 제공자 구현체
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@Service // 추가된 어노테이션
|
||||
@RequiredArgsConstructor
|
||||
public class StoreApiDataProvider implements StoreDataProvider {
|
||||
|
||||
|
||||
@ -1,68 +0,0 @@
|
||||
package com.won.smarketing.recommend.infrastructure.external;
|
||||
|
||||
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 java.time.Duration;
|
||||
|
||||
/**
|
||||
* 날씨 API 데이터 제공자 구현체
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WeatherApiDataProvider implements WeatherDataProvider {
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
@Value("${external.weather-api.api-key}")
|
||||
private String weatherApiKey;
|
||||
|
||||
@Value("${external.weather-api.timeout}")
|
||||
private int timeout;
|
||||
|
||||
@Override
|
||||
@Cacheable(value = "weatherData", key = "#location")
|
||||
public WeatherData getCurrentWeather(String location) {
|
||||
try {
|
||||
log.debug("날씨 정보 조회: location={}", location);
|
||||
|
||||
// 개발 환경에서는 Mock 데이터 반환
|
||||
if (weatherApiKey.equals("dummy-key")) {
|
||||
return createMockWeatherData(location);
|
||||
}
|
||||
|
||||
// 실제 날씨 API 호출 (향후 구현)
|
||||
return callWeatherApi(location);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("날씨 정보 조회 실패, Mock 데이터 사용: location={}", location, e);
|
||||
return createMockWeatherData(location);
|
||||
}
|
||||
}
|
||||
|
||||
private WeatherData callWeatherApi(String location) {
|
||||
// 실제 OpenWeatherMap API 호출 로직 (향후 구현)
|
||||
log.info("실제 날씨 API 호출: {}", location);
|
||||
return createMockWeatherData(location);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,8 @@
|
||||
package com.won.smarketing.recommend.entity;
|
||||
package com.won.smarketing.recommend.infrastructure.persistence;
|
||||
|
||||
import com.won.smarketing.recommend.domain.model.MarketingTip;
|
||||
import com.won.smarketing.recommend.domain.model.StoreData;
|
||||
import com.won.smarketing.recommend.domain.model.TipId;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
@ -11,7 +14,7 @@ import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 마케팅 팁 JPA 엔티티
|
||||
* 마케팅 팁 JPA 엔티티 (날씨 정보 제거)
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "marketing_tips")
|
||||
@ -29,20 +32,10 @@ public class MarketingTipEntity {
|
||||
@Column(name = "store_id", nullable = false)
|
||||
private Long storeId;
|
||||
|
||||
@Column(name = "tip_content", columnDefinition = "TEXT", nullable = false)
|
||||
@Column(name = "tip_content", nullable = false, length = 2000)
|
||||
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;
|
||||
|
||||
@ -55,4 +48,32 @@ public class MarketingTipEntity {
|
||||
@CreatedDate
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
|
||||
public static MarketingTipEntity fromDomain(MarketingTip marketingTip) {
|
||||
return MarketingTipEntity.builder()
|
||||
.id(marketingTip.getId() != null ? marketingTip.getId().getValue() : null)
|
||||
.storeId(marketingTip.getStoreId())
|
||||
.tipContent(marketingTip.getTipContent())
|
||||
.storeName(marketingTip.getStoreData().getStoreName())
|
||||
.businessType(marketingTip.getStoreData().getBusinessType())
|
||||
.storeLocation(marketingTip.getStoreData().getLocation())
|
||||
.createdAt(marketingTip.getCreatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
public MarketingTip toDomain() {
|
||||
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)
|
||||
.storeData(storeData)
|
||||
.createdAt(this.createdAt)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,18 @@
|
||||
package com.won.smarketing.recommend.infrastructure.persistence;
|
||||
|
||||
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;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* 마케팅 팁 JPA 레포지토리
|
||||
*/
|
||||
public interface MarketingTipJpaRepository extends JpaRepository<com.won.smarketing.recommend.entity.MarketingTipEntity, Long> {
|
||||
|
||||
@Repository
|
||||
public interface MarketingTipJpaRepository extends JpaRepository<MarketingTipEntity, Long> {
|
||||
|
||||
@Query("SELECT m FROM MarketingTipEntity m WHERE m.storeId = :storeId ORDER BY m.createdAt DESC")
|
||||
Page<com.won.smarketing.recommend.entity.MarketingTipEntity> findByStoreIdOrderByCreatedAtDesc(@Param("storeId") Long storeId, Pageable pageable);
|
||||
}
|
||||
Page<MarketingTipEntity> findByStoreIdOrderByCreatedAtDesc(@Param("storeId") Long storeId, Pageable pageable);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.won.smarketing.recommend.infrastructure.persistence;
|
||||
|
||||
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;
|
||||
@ -9,18 +10,18 @@ import org.springframework.stereotype.Repository;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* JPA 마케팅 팁 레포지토리 구현체
|
||||
* 마케팅 팁 레포지토리 구현체
|
||||
*/
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
public class JpaMarketingTipRepository implements MarketingTipRepository {
|
||||
public class MarketingTipRepositoryImpl 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);
|
||||
MarketingTipEntity entity = MarketingTipEntity.fromDomain(marketingTip);
|
||||
MarketingTipEntity savedEntity = jpaRepository.save(entity);
|
||||
return savedEntity.toDomain();
|
||||
}
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
//package com.won.smarketing.recommend.presentation.controller;
|
||||
//
|
||||
//import org.springframework.web.bind.annotation.GetMapping;
|
||||
//import org.springframework.web.bind.annotation.RestController;
|
||||
//
|
||||
//import java.time.LocalDateTime;
|
||||
//import java.util.Map;
|
||||
//
|
||||
///**
|
||||
// * 헬스체크 컨트롤러
|
||||
// */
|
||||
//@RestController
|
||||
//public class HealthController {
|
||||
//
|
||||
// @GetMapping("/health")
|
||||
// public Map<String, Object> health() {
|
||||
// return Map.of(
|
||||
// "status", "UP",
|
||||
// "service", "ai-recommend-service",
|
||||
// "timestamp", LocalDateTime.now(),
|
||||
// "message", "AI 추천 서비스가 정상 동작 중입니다.",
|
||||
// "features", Map.of(
|
||||
// "store_integration", "매장 서비스 연동",
|
||||
// "python_ai_integration", "Python AI 서비스 연동",
|
||||
// "fallback_support", "Fallback 팁 생성 지원"
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
//}
|
||||
// }
|
||||
//
|
||||
// } catch (Exception e) {
|
||||
// log.error("매장 정보 조회 실패, Mock 데이터 반환: storeId={}", storeId, e);
|
||||
// return createMockStoreData(storeId);
|
||||
@ -29,49 +29,49 @@ public class RecommendationController {
|
||||
private final MarketingTipUseCase marketingTipUseCase;
|
||||
|
||||
@Operation(
|
||||
summary = "AI 마케팅 팁 생성",
|
||||
description = "매장 정보와 환경 데이터를 기반으로 AI 마케팅 팁을 생성합니다."
|
||||
summary = "AI 마케팅 팁 생성",
|
||||
description = "매장 정보를 기반으로 Python AI 서비스에서 마케팅 팁을 생성합니다."
|
||||
)
|
||||
@PostMapping("/marketing-tips")
|
||||
public ResponseEntity<ApiResponse<MarketingTipResponse>> generateMarketingTips(
|
||||
@Parameter(description = "마케팅 팁 생성 요청") @Valid @RequestBody MarketingTipRequest request) {
|
||||
|
||||
|
||||
log.info("AI 마케팅 팁 생성 요청: storeId={}", request.getStoreId());
|
||||
|
||||
|
||||
MarketingTipResponse response = marketingTipUseCase.generateMarketingTips(request);
|
||||
|
||||
|
||||
log.info("AI 마케팅 팁 생성 완료: tipId={}", response.getTipId());
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "마케팅 팁 이력 조회",
|
||||
description = "특정 매장의 마케팅 팁 생성 이력을 조회합니다."
|
||||
summary = "마케팅 팁 이력 조회",
|
||||
description = "특정 매장의 마케팅 팁 생성 이력을 조회합니다."
|
||||
)
|
||||
@GetMapping("/marketing-tips")
|
||||
public ResponseEntity<ApiResponse<Page<MarketingTipResponse>>> getMarketingTipHistory(
|
||||
@Parameter(description = "매장 ID") @RequestParam Long storeId,
|
||||
Pageable pageable) {
|
||||
|
||||
|
||||
log.info("마케팅 팁 이력 조회: storeId={}, page={}", storeId, pageable.getPageNumber());
|
||||
|
||||
|
||||
Page<MarketingTipResponse> response = marketingTipUseCase.getMarketingTipHistory(storeId, pageable);
|
||||
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "마케팅 팁 상세 조회",
|
||||
description = "특정 마케팅 팁의 상세 정보를 조회합니다."
|
||||
summary = "마케팅 팁 상세 조회",
|
||||
description = "특정 마케팅 팁의 상세 정보를 조회합니다."
|
||||
)
|
||||
@GetMapping("/marketing-tips/{tipId}")
|
||||
public ResponseEntity<ApiResponse<MarketingTipResponse>> getMarketingTip(
|
||||
@Parameter(description = "팁 ID") @PathVariable Long tipId) {
|
||||
|
||||
|
||||
log.info("마케팅 팁 상세 조회: tipId={}", tipId);
|
||||
|
||||
|
||||
MarketingTipResponse response = marketingTipUseCase.getMarketingTip(tipId);
|
||||
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
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<String, Object> parameters;
|
||||
private Map<String, Object> context; // 매장 정보, 과거 데이터 등
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
package com.won.smarketing.recommend.presentation.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 상세 AI 마케팅 팁 응답 DTO
|
||||
* AI 마케팅 팁과 함께 생성 시 사용된 환경 데이터도 포함합니다.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "상세 AI 마케팅 팁 응답")
|
||||
public class DetailedMarketingTipResponse {
|
||||
|
||||
@Schema(description = "팁 ID", example = "1")
|
||||
private Long tipId;
|
||||
|
||||
@Schema(description = "AI 생성 마케팅 팁 내용 (100자 이내)")
|
||||
private String tipContent;
|
||||
|
||||
@Schema(description = "팁 생성 시간", example = "2024-01-15T10:30:00")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Schema(description = "팁 생성 시 참고된 날씨 정보")
|
||||
private WeatherInfoDto weatherInfo;
|
||||
|
||||
@Schema(description = "팁 생성 시 참고된 매장 정보")
|
||||
private StoreInfoDto storeInfo;
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
package com.won.smarketing.recommend.presentation.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 에러 응답 DTO
|
||||
* AI 추천 서비스에서 발생하는 에러 정보를 전달합니다.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "에러 응답")
|
||||
public class ErrorResponseDto {
|
||||
|
||||
@Schema(description = "에러 코드", example = "AI_SERVICE_ERROR")
|
||||
private String errorCode;
|
||||
|
||||
@Schema(description = "에러 메시지", example = "AI 서비스 연결에 실패했습니다")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "에러 발생 시간", example = "2024-01-15T10:30:00")
|
||||
private LocalDateTime timestamp;
|
||||
|
||||
@Schema(description = "요청 경로", example = "/api/recommendation/marketing-tips")
|
||||
private String path;
|
||||
}
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
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.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* AI 마케팅 팁 생성을 위한 내부 요청 DTO
|
||||
* 애플리케이션 계층에서 AI 서비스 호출 시 사용됩니다.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "AI 마케팅 팁 생성 내부 요청")
|
||||
public class MarketingTipGenerationRequest {
|
||||
|
||||
@NotNull(message = "매장 정보는 필수입니다")
|
||||
@Schema(description = "매장 정보", required = true)
|
||||
private StoreInfoDto storeInfo;
|
||||
|
||||
@Schema(description = "현재 날씨 정보")
|
||||
private WeatherInfoDto weatherInfo;
|
||||
|
||||
@Schema(description = "팁 생성 옵션", example = "일반")
|
||||
private String tipType;
|
||||
}
|
||||
@ -23,4 +23,4 @@ public class MarketingTipRequest {
|
||||
|
||||
@Schema(description = "추가 요청사항", example = "여름철 음료 프로모션에 집중해주세요")
|
||||
private String additionalRequirement;
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,30 +27,12 @@ public class MarketingTipResponse {
|
||||
@Schema(description = "AI 생성 마케팅 팁 내용")
|
||||
private String tipContent;
|
||||
|
||||
@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
|
||||
@ -65,4 +47,4 @@ public class MarketingTipResponse {
|
||||
@Schema(description = "위치", example = "서울시 강남구")
|
||||
private String location;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
package com.won.smarketing.recommend.presentation.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 매장 정보 DTO
|
||||
* AI 마케팅 팁 생성 시 매장 특성을 반영하기 위한 정보입니다.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "매장 정보")
|
||||
public class StoreInfoDto {
|
||||
|
||||
@Schema(description = "매장명", example = "카페 원더풀")
|
||||
private String storeName;
|
||||
|
||||
@Schema(description = "업종", example = "카페")
|
||||
private String businessType;
|
||||
|
||||
@Schema(description = "매장 위치", example = "서울시 강남구")
|
||||
private String location;
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
package com.won.smarketing.recommend.presentation.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 날씨 정보 DTO
|
||||
* AI 마케팅 팁 생성 시 참고되는 환경 데이터입니다.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "날씨 정보")
|
||||
public class WeatherInfoDto {
|
||||
|
||||
@Schema(description = "기온 (섭씨)", example = "23.5")
|
||||
private Double temperature;
|
||||
|
||||
@Schema(description = "날씨 상태", example = "맑음")
|
||||
private String condition;
|
||||
|
||||
@Schema(description = "습도 (%)", example = "65.0")
|
||||
private Double humidity;
|
||||
}
|
||||
@ -24,23 +24,23 @@ spring:
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
|
||||
ai:
|
||||
service:
|
||||
url: ${AI_SERVICE_URL:http://localhost:8080/ai}
|
||||
timeout: ${AI_SERVICE_TIMEOUT:30000}
|
||||
|
||||
|
||||
external:
|
||||
claude-ai:
|
||||
api-key: ${CLAUDE_AI_API_KEY:your-claude-api-key}
|
||||
base-url: ${CLAUDE_AI_BASE_URL:https://api.anthropic.com}
|
||||
model: ${CLAUDE_AI_MODEL:claude-3-sonnet-20240229}
|
||||
max-tokens: ${CLAUDE_AI_MAX_TOKENS:2000}
|
||||
weather-api:
|
||||
api-key: ${WEATHER_API_KEY:your-weather-api-key}
|
||||
base-url: ${WEATHER_API_BASE_URL:https://api.openweathermap.org/data/2.5}
|
||||
store-service:
|
||||
base-url: ${STORE_SERVICE_URL:http://localhost:8082}
|
||||
timeout: ${STORE_SERVICE_TIMEOUT:5000}
|
||||
python-ai-service:
|
||||
base-url: ${PYTHON_AI_SERVICE_URL:http://localhost:8090}
|
||||
api-key: ${PYTHON_AI_API_KEY:dummy-key}
|
||||
timeout: ${PYTHON_AI_TIMEOUT:30000}
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
@ -56,4 +56,4 @@ logging:
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:mySecretKeyForJWTTokenGenerationAndValidation123456789}
|
||||
access-token-validity: ${JWT_ACCESS_VALIDITY:3600000}
|
||||
refresh-token-validity: ${JWT_REFRESH_VALIDITY:604800000}
|
||||
refresh-token-validity: ${JWT_REFRESH_VALIDITY:604800000}
|
||||
Loading…
x
Reference in New Issue
Block a user