diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index b7b3d1b..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,5 +0,0 @@
-# 디폴트 무시된 파일
-/shelf/
-/workspace.xml
-# 환경에 따라 달라지는 Maven 홈 디렉터리
-/mavenHomeManager.xml
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
deleted file mode 100644
index 9018a0d..0000000
--- a/.idea/gradle.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 6ed36dd..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 35eb1dd..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 0000000..fc7acb6
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ "customColor": "",
+ "associatedIndex": 4
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+ false
+
+
+
+
+
+
+ 1749618504890
+
+
+ 1749618504890
+
+
+
+
\ No newline at end of file
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}
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java
index fec5d4e..dd8e603 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java
@@ -42,7 +42,7 @@ public class SnsContentService implements SnsContentUseCase {
public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request) {
// AI를 사용하여 SNS 콘텐츠 생성
String generatedContent = aiContentGenerator.generateSnsContent(request);
-
+
// 플랫폼에 맞는 해시태그 생성
Platform platform = Platform.fromString(request.getPlatform());
List hashtags = aiContentGenerator.generateHashtags(generatedContent, platform);
@@ -60,7 +60,7 @@ public class SnsContentService implements SnsContentUseCase {
// 임시 콘텐츠 생성 (저장하지 않음)
Content content = Content.builder()
- .contentType(ContentType.SNS_POST)
+// .contentType(ContentType.SNS_POST)
.platform(platform)
.title(request.getTitle())
.content(generatedContent)
@@ -88,7 +88,7 @@ public class SnsContentService implements SnsContentUseCase {
/**
* SNS 콘텐츠 저장
- *
+ *
* @param request SNS 콘텐츠 저장 요청
*/
@Override
@@ -107,7 +107,7 @@ public class SnsContentService implements SnsContentUseCase {
// 콘텐츠 엔티티 생성 및 저장
Content content = Content.builder()
- .contentType(ContentType.SNS_POST)
+// .contentType(ContentType.SNS_POST)
.platform(Platform.fromString(request.getPlatform()))
.title(request.getTitle())
.content(request.getContent())
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java
index 973b234..6bf2960 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java
@@ -1,3 +1,4 @@
+// marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java
package com.won.smarketing.content.application.usecase;
import com.won.smarketing.content.presentation.dto.PosterContentCreateRequest;
@@ -5,23 +6,21 @@ import com.won.smarketing.content.presentation.dto.PosterContentCreateResponse;
import com.won.smarketing.content.presentation.dto.PosterContentSaveRequest;
/**
- * 포스터 콘텐츠 관련 Use Case 인터페이스
- * 홍보 포스터 생성 및 저장 기능 정의
+ * 포스터 콘텐츠 관련 UseCase 인터페이스
+ * Clean Architecture의 Application Layer에서 비즈니스 로직 정의
*/
public interface PosterContentUseCase {
-
+
/**
* 포스터 콘텐츠 생성
- *
* @param request 포스터 콘텐츠 생성 요청
- * @return 생성된 포스터 콘텐츠 정보
+ * @return 포스터 콘텐츠 생성 응답
*/
PosterContentCreateResponse generatePosterContent(PosterContentCreateRequest request);
-
+
/**
* 포스터 콘텐츠 저장
- *
* @param request 포스터 콘텐츠 저장 요청
*/
void savePosterContent(PosterContentSaveRequest request);
-}
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/SnsContentUseCase.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/SnsContentUseCase.java
index e62902d..d2c6751 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/SnsContentUseCase.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/SnsContentUseCase.java
@@ -1,3 +1,4 @@
+// marketing-content/src/main/java/com/won/smarketing/content/application/usecase/SnsContentUseCase.java
package com.won.smarketing.content.application.usecase;
import com.won.smarketing.content.presentation.dto.SnsContentCreateRequest;
@@ -5,23 +6,21 @@ import com.won.smarketing.content.presentation.dto.SnsContentCreateResponse;
import com.won.smarketing.content.presentation.dto.SnsContentSaveRequest;
/**
- * SNS 콘텐츠 관련 Use Case 인터페이스
- * SNS 게시물 생성 및 저장 기능 정의
+ * SNS 콘텐츠 관련 UseCase 인터페이스
+ * Clean Architecture의 Application Layer에서 비즈니스 로직 정의
*/
public interface SnsContentUseCase {
-
+
/**
* SNS 콘텐츠 생성
- *
* @param request SNS 콘텐츠 생성 요청
- * @return 생성된 SNS 콘텐츠 정보
+ * @return SNS 콘텐츠 생성 응답
*/
SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request);
-
+
/**
* SNS 콘텐츠 저장
- *
* @param request SNS 콘텐츠 저장 요청
*/
void saveSnsContent(SnsContentSaveRequest request);
-}
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/JpaConfig.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/JpaConfig.java
deleted file mode 100644
index e95312d..0000000
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/JpaConfig.java
+++ /dev/null
@@ -1,18 +0,0 @@
-// marketing-content/src/main/java/com/won/smarketing/content/config/JpaConfig.java
-package com.won.smarketing.content.config;
-
-import org.springframework.boot.autoconfigure.domain.EntityScan;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
-
-/**
- * JPA 설정 클래스
- *
- * @author smarketing-team
- * @version 1.0
- */
-@Configuration
-@EntityScan(basePackages = "com.won.smarketing.content.infrastructure.entity")
-@EnableJpaRepositories(basePackages = "com.won.smarketing.content.infrastructure.repository")
-public class JpaConfig {
-}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Content.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Content.java
index 4e95d02..549520c 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Content.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Content.java
@@ -46,7 +46,7 @@ public class Content {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
- @Column(name = "content_id")
+ @Column(name = "id")
private Long id;
// ==================== 콘텐츠 분류 ====================
@@ -97,8 +97,7 @@ public class Content {
private ContentStatus status = ContentStatus.DRAFT;
// ==================== AI 생성 조건 (Embedded) ====================
-
- @Embedded
+ //@Embedded
@AttributeOverrides({
@AttributeOverride(name = "toneAndManner", column = @Column(name = "tone_and_manner", length = 50)),
@AttributeOverride(name = "promotionType", column = @Column(name = "promotion_type", length = 50)),
@@ -191,15 +190,15 @@ public class Content {
* @param status 새로운 상태
* @throws IllegalStateException 잘못된 상태 전환인 경우
*/
- public void changeStatus(ContentStatus status) {
- validateStatusTransition(this.status, status);
-
- if (status == ContentStatus.PUBLISHED) {
- validateForPublication();
- }
-
- this.status = status;
- }
+// public void changeStatus(ContentStatus status) {
+// validateStatusTransition(this.status, status);
+//
+// if (status == ContentStatus.PUBLISHED) {
+// validateForPublication();
+// }
+//
+// this.status = status;
+// }
/**
* 홍보 기간 설정
@@ -352,9 +351,9 @@ public class Content {
*
* @return SNS 게시물이면 true
*/
- public boolean isSnsContent() {
- return this.contentType == ContentType.SNS_POST;
- }
+// public boolean isSnsContent() {
+// return this.contentType == ContentType.SNS_POST;
+// }
/**
* 포스터 콘텐츠 여부 확인
@@ -424,11 +423,11 @@ public class Content {
/**
* 상태 전환 유효성 검증
*/
- private void validateStatusTransition(ContentStatus from, ContentStatus to) {
- if (from == ContentStatus.ARCHIVED && to != ContentStatus.PUBLISHED) {
- throw new IllegalStateException("보관된 콘텐츠는 발행 상태로만 변경할 수 있습니다.");
- }
- }
+// private void validateStatusTransition(ContentStatus from, ContentStatus to) {
+// if (from == ContentStatus.ARCHIVED && to != ContentStatus.PUBLISHED) {
+// throw new IllegalStateException("보관된 콘텐츠는 발행 상태로만 변경할 수 있습니다.");
+// }
+// }
/**
* 발행을 위한 유효성 검증
@@ -502,7 +501,7 @@ public class Content {
public static Content createSnsContent(String title, String content, Platform platform,
Long storeId, CreationConditions conditions) {
Content snsContent = Content.builder()
- .contentType(ContentType.SNS_POST)
+// .contentType(ContentType.SNS_POST)
.platform(platform)
.title(title)
.content(content)
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentId.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentId.java
index 13bb3b0..25220a8 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentId.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentId.java
@@ -1,30 +1,53 @@
+// marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentId.java
package com.won.smarketing.content.domain.model;
-import lombok.*;
+import jakarta.persistence.Embeddable;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.Objects;
/**
- * 콘텐츠 식별자 값 객체
- * 콘텐츠의 고유 식별자를 나타내는 도메인 객체
+ * 콘텐츠 ID 값 객체
+ * Clean Architecture의 Domain Layer에 위치하는 식별자
*/
+@Embeddable
@Getter
-@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
-@EqualsAndHashCode
public class ContentId {
private Long value;
/**
* ContentId 생성 팩토리 메서드
- *
- * @param value 식별자 값
+ * @param value ID 값
* @return ContentId 인스턴스
*/
public static ContentId of(Long value) {
if (value == null || value <= 0) {
- throw new IllegalArgumentException("ContentId는 양수여야 합니다.");
+ throw new IllegalArgumentException("ContentId 값은 양수여야 합니다.");
}
return new ContentId(value);
}
-}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ContentId contentId = (ContentId) o;
+ return Objects.equals(value, contentId.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(value);
+ }
+
+ @Override
+ public String toString() {
+ return "ContentId{" + value + '}';
+ }
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentStatus.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentStatus.java
index c40ec47..b235310 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentStatus.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentStatus.java
@@ -1,3 +1,4 @@
+// marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentStatus.java
package com.won.smarketing.content.domain.model;
import lombok.Getter;
@@ -5,35 +6,35 @@ import lombok.RequiredArgsConstructor;
/**
* 콘텐츠 상태 열거형
- * 콘텐츠의 생명주기 상태 정의
+ * Clean Architecture의 Domain Layer에 위치하는 비즈니스 규칙
*/
@Getter
@RequiredArgsConstructor
public enum ContentStatus {
-
+
DRAFT("임시저장"),
- PUBLISHED("발행됨"),
- ARCHIVED("보관됨");
+ PUBLISHED("게시됨"),
+ SCHEDULED("예약됨"),
+ DELETED("삭제됨"),
+ PROCESSING("처리중");
private final String displayName;
/**
* 문자열로부터 ContentStatus 변환
- *
- * @param status 상태 문자열
- * @return ContentStatus
+ * @param value 문자열 값
+ * @return ContentStatus enum
+ * @throws IllegalArgumentException 유효하지 않은 값인 경우
*/
- public static ContentStatus fromString(String status) {
- if (status == null) {
- return DRAFT;
+ public static ContentStatus fromString(String value) {
+ if (value == null) {
+ throw new IllegalArgumentException("ContentStatus 값은 null일 수 없습니다.");
}
-
- for (ContentStatus s : ContentStatus.values()) {
- if (s.name().equalsIgnoreCase(status)) {
- return s;
- }
+
+ try {
+ return ContentStatus.valueOf(value.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("유효하지 않은 ContentStatus 값입니다: " + value);
}
-
- throw new IllegalArgumentException("알 수 없는 콘텐츠 상태: " + status);
}
-}
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentType.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentType.java
index dd91b91..f70228b 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentType.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentType.java
@@ -1,3 +1,4 @@
+// marketing-content/src/main/java/com/won/smarketing/content/domain/model/ContentType.java
package com.won.smarketing.content.domain.model;
import lombok.Getter;
@@ -5,34 +6,34 @@ import lombok.RequiredArgsConstructor;
/**
* 콘텐츠 타입 열거형
- * 지원되는 마케팅 콘텐츠 유형 정의
+ * Clean Architecture의 Domain Layer에 위치하는 비즈니스 규칙
*/
@Getter
@RequiredArgsConstructor
public enum ContentType {
-
- SNS_POST("SNS 게시물"),
- POSTER("홍보 포스터");
+
+ SNS("SNS 게시물"),
+ POSTER("홍보 포스터"),
+ VIDEO("동영상"),
+ BLOG("블로그 포스트");
private final String displayName;
/**
* 문자열로부터 ContentType 변환
- *
- * @param type 타입 문자열
- * @return ContentType
+ * @param value 문자열 값
+ * @return ContentType enum
+ * @throws IllegalArgumentException 유효하지 않은 값인 경우
*/
- public static ContentType fromString(String type) {
- if (type == null) {
- return null;
+ public static ContentType fromString(String value) {
+ if (value == null) {
+ throw new IllegalArgumentException("ContentType 값은 null일 수 없습니다.");
}
-
- for (ContentType contentType : ContentType.values()) {
- if (contentType.name().equalsIgnoreCase(type)) {
- return contentType;
- }
+
+ try {
+ return ContentType.valueOf(value.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("유효하지 않은 ContentType 값입니다: " + value);
}
-
- throw new IllegalArgumentException("알 수 없는 콘텐츠 타입: " + type);
}
-}
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/CreationConditions.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/CreationConditions.java
index cf3c04e..cb1f914 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/CreationConditions.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/CreationConditions.java
@@ -1,66 +1,68 @@
+// marketing-content/src/main/java/com/won/smarketing/content/domain/model/CreationConditions.java
package com.won.smarketing.content.domain.model;
-import lombok.*;
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
import java.time.LocalDate;
/**
* 콘텐츠 생성 조건 도메인 모델
- * AI 콘텐츠 생성 시 사용되는 조건 정보
+ * Clean Architecture의 Domain Layer에 위치하는 값 객체
*/
+@Entity
+@Table(name = "contents_conditions")
@Getter
-@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@NoArgsConstructor
@AllArgsConstructor
-@Builder(toBuilder = true)
+@Builder
public class CreationConditions {
- /**
- * 홍보 대상 카테고리
- */
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ //@OneToOne(mappedBy = "creationConditions")
+ @Column(name = "content", length = 100)
+ private Content content;
+
+ @Column(name = "category", length = 100)
private String category;
- /**
- * 특별 요구사항
- */
+ @Column(name = "requirement", columnDefinition = "TEXT")
private String requirement;
- /**
- * 톤앤매너
- */
+ @Column(name = "tone_and_manner", length = 100)
private String toneAndManner;
- /**
- * 감정 강도
- */
+ @Column(name = "emotion_intensity", length = 100)
private String emotionIntensity;
- /**
- * 이벤트명
- */
+ @Column(name = "event_name", length = 200)
private String eventName;
- /**
- * 홍보 시작일
- */
+ @Column(name = "start_date")
private LocalDate startDate;
- /**
- * 홍보 종료일
- */
+ @Column(name = "end_date")
private LocalDate endDate;
- /**
- * 사진 스타일 (포스터용)
- */
+ @Column(name = "photo_style", length = 100)
private String photoStyle;
- /**
- * 타겟 고객
- */
- private String targetAudience;
-
- /**
- * 프로모션 타입
- */
+ @Column(name = "promotionType", length = 100)
private String promotionType;
-}
+
+ public CreationConditions(String category, String requirement, String toneAndManner, String emotionIntensity, String eventName, LocalDate startDate, LocalDate endDate, String photoStyle, String promotionType) {
+ }
+// /**
+// * 콘텐츠와의 연관관계 설정
+// * @param content 연관된 콘텐츠
+// */
+// public void setContent(Content content) {
+// this.content = content;
+// }
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Platform.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Platform.java
index acd6b33..66e266c 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Platform.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Platform.java
@@ -1,3 +1,4 @@
+// marketing-content/src/main/java/com/won/smarketing/content/domain/model/Platform.java
package com.won.smarketing.content.domain.model;
import lombok.Getter;
@@ -5,35 +6,36 @@ import lombok.RequiredArgsConstructor;
/**
* 플랫폼 열거형
- * 콘텐츠가 게시될 플랫폼 정의
+ * Clean Architecture의 Domain Layer에 위치하는 비즈니스 규칙
*/
@Getter
@RequiredArgsConstructor
public enum Platform {
-
+
INSTAGRAM("인스타그램"),
NAVER_BLOG("네이버 블로그"),
- GENERAL("범용");
+ FACEBOOK("페이스북"),
+ KAKAO_STORY("카카오스토리"),
+ YOUTUBE("유튜브"),
+ GENERAL("일반");
private final String displayName;
/**
* 문자열로부터 Platform 변환
- *
- * @param platform 플랫폼 문자열
- * @return Platform
+ * @param value 문자열 값
+ * @return Platform enum
+ * @throws IllegalArgumentException 유효하지 않은 값인 경우
*/
- public static Platform fromString(String platform) {
- if (platform == null) {
- return GENERAL;
+ public static Platform fromString(String value) {
+ if (value == null) {
+ throw new IllegalArgumentException("Platform 값은 null일 수 없습니다.");
}
-
- for (Platform p : Platform.values()) {
- if (p.name().equalsIgnoreCase(platform)) {
- return p;
- }
+
+ try {
+ return Platform.valueOf(value.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("유효하지 않은 Platform 값입니다: " + value);
}
-
- throw new IllegalArgumentException("알 수 없는 플랫폼: " + platform);
}
-}
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/repository/ContentRepository.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/repository/ContentRepository.java
index 818506f..a2bfc43 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/repository/ContentRepository.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/repository/ContentRepository.java
@@ -1,40 +1,36 @@
+// marketing-content/src/main/java/com/won/smarketing/content/domain/repository/ContentRepository.java
package com.won.smarketing.content.domain.repository;
import com.won.smarketing.content.domain.model.Content;
import com.won.smarketing.content.domain.model.ContentId;
import com.won.smarketing.content.domain.model.ContentType;
import com.won.smarketing.content.domain.model.Platform;
-import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/**
- * 콘텐츠 저장소 인터페이스
- * 콘텐츠 도메인의 데이터 접근 추상화
+ * 콘텐츠 리포지토리 인터페이스
+ * Clean Architecture의 Domain Layer에서 데이터 접근 정의
*/
-@Repository
public interface ContentRepository {
-
+
/**
* 콘텐츠 저장
- *
* @param content 저장할 콘텐츠
* @return 저장된 콘텐츠
*/
Content save(Content content);
-
+
/**
- * 콘텐츠 ID로 조회
- *
+ * ID로 콘텐츠 조회
* @param id 콘텐츠 ID
- * @return 콘텐츠 (Optional)
+ * @return 조회된 콘텐츠
*/
Optional findById(ContentId id);
-
+
/**
* 필터 조건으로 콘텐츠 목록 조회
- *
* @param contentType 콘텐츠 타입
* @param platform 플랫폼
* @param period 기간
@@ -42,19 +38,17 @@ public interface ContentRepository {
* @return 콘텐츠 목록
*/
List findByFilters(ContentType contentType, Platform platform, String period, String sortBy);
-
+
/**
* 진행 중인 콘텐츠 목록 조회
- *
* @param period 기간
* @return 진행 중인 콘텐츠 목록
*/
List findOngoingContents(String period);
-
+
/**
- * 콘텐츠 삭제
- *
+ * ID로 콘텐츠 삭제
* @param id 삭제할 콘텐츠 ID
*/
void deleteById(ContentId id);
-}
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/repository/SpringDataContentRepository.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/repository/SpringDataContentRepository.java
new file mode 100644
index 0000000..d3a6e42
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/repository/SpringDataContentRepository.java
@@ -0,0 +1,38 @@
+package com.won.smarketing.content.domain.repository;
+import com.won.smarketing.content.infrastructure.entity.ContentEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/**
+ * Spring Data JPA ContentRepository
+ * JPA 기반 콘텐츠 데이터 접근
+ */
+@Repository
+public interface SpringDataContentRepository extends JpaRepository {
+
+ /**
+ * 매장별 콘텐츠 조회
+ *
+ * @param storeId 매장 ID
+ * @return 콘텐츠 목록
+ */
+ List findByStoreId(Long storeId);
+
+ /**
+ * 콘텐츠 타입별 조회
+ *
+ * @param contentType 콘텐츠 타입
+ * @return 콘텐츠 목록
+ */
+ List findByContentType(String contentType);
+
+ /**
+ * 플랫폼별 조회
+ *
+ * @param platform 플랫폼
+ * @return 콘텐츠 목록
+ */
+ List findByPlatform(String platform);
+}
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentConditionsJpaEntity.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentConditionsJpaEntity.java
index 17f49f8..940bbba 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentConditionsJpaEntity.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentConditionsJpaEntity.java
@@ -1,6 +1,8 @@
+// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentConditionsJpaEntity.java
package com.won.smarketing.content.infrastructure.entity;
import jakarta.persistence.*;
+import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@@ -8,24 +10,20 @@ import lombok.Setter;
import java.time.LocalDate;
/**
- * 콘텐츠 조건 JPA 엔티티
- *
- * @author smarketing-team
- * @version 1.0
+ * 콘텐츠 생성 조건 JPA 엔티티
*/
@Entity
-@Table(name = "contents_conditions")
+@Table(name = "content_conditions")
@Getter
@Setter
-@NoArgsConstructor
public class ContentConditionsJpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
- @OneToOne
- @JoinColumn(name = "content_id")
+ @OneToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "content_id", nullable = false)
private ContentJpaEntity content;
@Column(name = "category", length = 100)
@@ -37,7 +35,7 @@ public class ContentConditionsJpaEntity {
@Column(name = "tone_and_manner", length = 100)
private String toneAndManner;
- @Column(name = "emotion_intensity", length = 100)
+ @Column(name = "emotion_intensity", length = 50)
private String emotionIntensity;
@Column(name = "event_name", length = 200)
@@ -52,9 +50,9 @@ public class ContentConditionsJpaEntity {
@Column(name = "photo_style", length = 100)
private String photoStyle;
- @Column(name = "TargetAudience", length = 100)
+ @Column(name = "target_audience", length = 200)
private String targetAudience;
- @Column(name = "PromotionType", length = 100)
- private String PromotionType;
-}
+ @Column(name = "promotion_type", length = 100)
+ private String promotionType;
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentEntity.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentEntity.java
new file mode 100644
index 0000000..ba941d4
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentEntity.java
@@ -0,0 +1,60 @@
+package com.won.smarketing.content.infrastructure.entity;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.LocalDateTime;
+
+/**
+ * 콘텐츠 엔티티
+ * 콘텐츠 정보를 데이터베이스에 저장하기 위한 JPA 엔티티
+ */
+@Entity
+@Table(name = "contents")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@EntityListeners(AuditingEntityListener.class)
+public class ContentEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(name = "content_type", nullable = false)
+ private String contentType;
+
+ @Column(name = "platform", nullable = false)
+ private String platform;
+
+ @Column(name = "title", nullable = false)
+ private String title;
+
+ @Column(name = "content", columnDefinition = "TEXT")
+ private String content;
+
+ @Column(name = "hashtags")
+ private String hashtags;
+
+ @Column(name = "images", columnDefinition = "TEXT")
+ private String images;
+
+ @Column(name = "status", nullable = false)
+ private String status;
+
+ @Column(name = "store_id", nullable = false)
+ private Long storeId;
+
+ @CreatedDate
+ @Column(name = "created_at", updatable = false)
+ private LocalDateTime createdAt;
+
+ @LastModifiedDate
+ @Column(name = "updated_at")
+ private LocalDateTime updatedAt;
+}
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentJpaEntity.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentJpaEntity.java
index 7f87560..2bd786a 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentJpaEntity.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentJpaEntity.java
@@ -1,27 +1,24 @@
-// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/entity/ContentJpaEntity.java
package com.won.smarketing.content.infrastructure.entity;
import jakarta.persistence.*;
+import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
-import org.hibernate.annotations.CreationTimestamp;
-import org.hibernate.annotations.UpdateTimestamp;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
-import java.util.List;
/**
* 콘텐츠 JPA 엔티티
- *
- * @author smarketing-team
- * @version 1.0
*/
@Entity
@Table(name = "contents")
@Getter
@Setter
-@NoArgsConstructor
+@EntityListeners(AuditingEntityListener.class)
public class ContentJpaEntity {
@Id
@@ -43,24 +40,24 @@ public class ContentJpaEntity {
@Column(name = "content", columnDefinition = "TEXT")
private String content;
- @Column(name = "hashtags", columnDefinition = "JSON")
+ @Column(name = "hashtags", columnDefinition = "TEXT")
private String hashtags;
- @Column(name = "images", columnDefinition = "JSON")
+ @Column(name = "images", columnDefinition = "TEXT")
private String images;
- @Column(name = "status", length = 50)
+ @Column(name = "status", nullable = false, length = 20)
private String status;
- @CreationTimestamp
- @Column(name = "created_at")
+ @CreatedDate
+ @Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
- @UpdateTimestamp
+ @LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
- // 연관 엔티티
+ // CreationConditions와의 관계 - OneToOne으로 별도 엔티티로 관리
@OneToOne(mappedBy = "content", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private ContentConditionsJpaEntity conditions;
}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/AiContentGenerator.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/AiContentGenerator.java
new file mode 100644
index 0000000..b1d0e6d
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/AiContentGenerator.java
@@ -0,0 +1,32 @@
+// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/AiContentGenerator.java
+package com.won.smarketing.content.infrastructure.external;
+
+import com.won.smarketing.content.domain.model.Platform;
+import com.won.smarketing.content.domain.model.CreationConditions;
+
+import java.util.List;
+
+/**
+ * AI 콘텐츠 생성 인터페이스
+ * Clean Architecture의 Infrastructure Layer에서 외부 AI 서비스와의 연동 정의
+ */
+public interface AiContentGenerator {
+
+ /**
+ * SNS 콘텐츠 생성
+ * @param title 제목
+ * @param category 카테고리
+ * @param platform 플랫폼
+ * @param conditions 생성 조건
+ * @return 생성된 콘텐츠 텍스트
+ */
+ String generateSnsContent(String title, String category, Platform platform, CreationConditions conditions);
+
+ /**
+ * 해시태그 생성
+ * @param content 콘텐츠 내용
+ * @param platform 플랫폼
+ * @return 생성된 해시태그 목록
+ */
+ List generateHashtags(String content, Platform platform);
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/AiPosterGenerator.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/AiPosterGenerator.java
new file mode 100644
index 0000000..8bbe931
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/AiPosterGenerator.java
@@ -0,0 +1,29 @@
+// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/AiPosterGenerator.java
+package com.won.smarketing.content.infrastructure.external;
+
+import com.won.smarketing.content.domain.model.CreationConditions;
+
+import java.util.Map;
+
+/**
+ * AI 포스터 생성 인터페이스
+ * Clean Architecture의 Infrastructure Layer에서 외부 AI 서비스와의 연동 정의
+ */
+public interface AiPosterGenerator {
+
+ /**
+ * 포스터 이미지 생성
+ * @param title 제목
+ * @param category 카테고리
+ * @param conditions 생성 조건
+ * @return 생성된 포스터 이미지 URL
+ */
+ String generatePoster(String title, String category, CreationConditions conditions);
+
+ /**
+ * 포스터 다양한 사이즈 생성
+ * @param originalImage 원본 이미지 URL
+ * @return 사이즈별 이미지 URL 맵
+ */
+ Map generatePosterSizes(String originalImage);
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java
new file mode 100644
index 0000000..5cf42a4
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java
@@ -0,0 +1,125 @@
+// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java
+package com.won.smarketing.content.infrastructure.external;
+
+import com.won.smarketing.content.domain.model.Platform;
+import com.won.smarketing.content.domain.model.CreationConditions;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Claude AI를 활용한 콘텐츠 생성 구현체
+ * Clean Architecture의 Infrastructure Layer에 위치
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class ClaudeAiContentGenerator implements AiContentGenerator {
+
+ /**
+ * SNS 콘텐츠 생성
+ * Claude AI API를 호출하여 SNS 게시물을 생성합니다.
+ *
+ * @param title 제목
+ * @param category 카테고리
+ * @param platform 플랫폼
+ * @param conditions 생성 조건
+ * @return 생성된 콘텐츠 텍스트
+ */
+ @Override
+ public String generateSnsContent(String title, String category, Platform platform, CreationConditions conditions) {
+ try {
+ // Claude AI API 호출 로직 (실제 구현에서는 HTTP 클라이언트를 사용)
+ String prompt = buildContentPrompt(title, category, platform, conditions);
+
+ // TODO: 실제 Claude AI API 호출
+ // 현재는 더미 데이터 반환
+ return generateDummySnsContent(title, platform);
+
+ } catch (Exception e) {
+ log.error("AI 콘텐츠 생성 실패: {}", e.getMessage(), e);
+ return generateFallbackContent(title, platform);
+ }
+ }
+
+ /**
+ * 해시태그 생성
+ * 콘텐츠 내용을 분석하여 관련 해시태그를 생성합니다.
+ *
+ * @param content 콘텐츠 내용
+ * @param platform 플랫폼
+ * @return 생성된 해시태그 목록
+ */
+ @Override
+ public List generateHashtags(String content, Platform platform) {
+ try {
+ // TODO: 실제 Claude AI API 호출하여 해시태그 생성
+ // 현재는 더미 데이터 반환
+ return generateDummyHashtags(platform);
+
+ } catch (Exception e) {
+ log.error("해시태그 생성 실패: {}", e.getMessage(), e);
+ return Arrays.asList("#맛집", "#신메뉴", "#추천");
+ }
+ }
+
+ /**
+ * AI 프롬프트 생성
+ */
+ private String buildContentPrompt(String title, String category, Platform platform, CreationConditions conditions) {
+ StringBuilder prompt = new StringBuilder();
+ prompt.append("다음 조건에 맞는 ").append(platform.getDisplayName()).append(" 게시물을 작성해주세요:\n");
+ prompt.append("제목: ").append(title).append("\n");
+ prompt.append("카테고리: ").append(category).append("\n");
+
+ if (conditions.getRequirement() != null) {
+ prompt.append("요구사항: ").append(conditions.getRequirement()).append("\n");
+ }
+ if (conditions.getToneAndManner() != null) {
+ prompt.append("톤앤매너: ").append(conditions.getToneAndManner()).append("\n");
+ }
+ if (conditions.getEmotionIntensity() != null) {
+ prompt.append("감정 강도: ").append(conditions.getEmotionIntensity()).append("\n");
+ }
+
+ return prompt.toString();
+ }
+
+ /**
+ * 더미 SNS 콘텐츠 생성 (개발용)
+ */
+ private String generateDummySnsContent(String title, Platform platform) {
+ switch (platform) {
+ case INSTAGRAM:
+ return String.format("🎉 %s\n\n맛있는 순간을 놓치지 마세요! 새로운 맛의 경험이 여러분을 기다리고 있어요. 따뜻한 분위기에서 즐기는 특별한 시간을 만들어보세요.\n\n📍 지금 바로 방문해보세요!", title);
+ case NAVER_BLOG:
+ return String.format("안녕하세요! 오늘은 %s에 대해 소개해드리려고 해요.\n\n정성스럽게 준비한 새로운 메뉴로 고객 여러분께 더 나은 경험을 선사하고 싶습니다. 많은 관심과 사랑 부탁드려요!", title);
+ default:
+ return String.format("%s - 새로운 경험을 만나보세요!", title);
+ }
+ }
+
+ /**
+ * 더미 해시태그 생성 (개발용)
+ */
+ private List generateDummyHashtags(Platform platform) {
+ switch (platform) {
+ case INSTAGRAM:
+ return Arrays.asList("#맛집", "#신메뉴", "#인스타그램", "#데일리", "#추천", "#음식스타그램");
+ case NAVER_BLOG:
+ return Arrays.asList("#맛집", "#리뷰", "#추천", "#신메뉴", "#블로그");
+ default:
+ return Arrays.asList("#맛집", "#신메뉴", "#추천");
+ }
+ }
+
+ /**
+ * 폴백 콘텐츠 생성 (AI 서비스 실패 시)
+ */
+ private String generateFallbackContent(String title, Platform platform) {
+ return String.format("🎉 %s\n\n새로운 소식을 전해드립니다. 많은 관심 부탁드려요!", title);
+ }
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiPosterGenerator.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiPosterGenerator.java
new file mode 100644
index 0000000..a667545
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiPosterGenerator.java
@@ -0,0 +1,118 @@
+// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiPosterGenerator.java
+package com.won.smarketing.content.infrastructure.external;
+
+import com.won.smarketing.content.domain.model.CreationConditions;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Claude AI를 활용한 포스터 생성 구현체
+ * Clean Architecture의 Infrastructure Layer에 위치
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class ClaudeAiPosterGenerator implements AiPosterGenerator {
+
+ /**
+ * 포스터 이미지 생성
+ * Claude AI API를 호출하여 홍보 포스터를 생성합니다.
+ *
+ * @param title 제목
+ * @param category 카테고리
+ * @param conditions 생성 조건
+ * @return 생성된 포스터 이미지 URL
+ */
+ @Override
+ public String generatePoster(String title, String category, CreationConditions conditions) {
+ try {
+ // Claude AI API 호출 로직 (실제 구현에서는 HTTP 클라이언트를 사용)
+ String prompt = buildPosterPrompt(title, category, conditions);
+
+ // TODO: 실제 Claude AI API 호출
+ // 현재는 더미 데이터 반환
+ return generateDummyPosterUrl(title);
+
+ } catch (Exception e) {
+ log.error("AI 포스터 생성 실패: {}", e.getMessage(), e);
+ return generateFallbackPosterUrl();
+ }
+ }
+
+ /**
+ * 포스터 다양한 사이즈 생성
+ * 원본 포스터를 기반으로 다양한 사이즈의 포스터를 생성합니다.
+ *
+ * @param originalImage 원본 이미지 URL
+ * @return 사이즈별 이미지 URL 맵
+ */
+ @Override
+ public Map generatePosterSizes(String originalImage) {
+ try {
+ // TODO: 실제 이미지 리사이징 API 호출
+ // 현재는 더미 데이터 반환
+ return generateDummyPosterSizes(originalImage);
+
+ } catch (Exception e) {
+ log.error("포스터 사이즈 생성 실패: {}", e.getMessage(), e);
+ return new HashMap<>();
+ }
+ }
+
+ /**
+ * AI 포스터 프롬프트 생성
+ */
+ private String buildPosterPrompt(String title, String category, CreationConditions conditions) {
+ StringBuilder prompt = new StringBuilder();
+ prompt.append("다음 조건에 맞는 홍보 포스터를 생성해주세요:\n");
+ prompt.append("제목: ").append(title).append("\n");
+ prompt.append("카테고리: ").append(category).append("\n");
+
+ if (conditions.getPhotoStyle() != null) {
+ prompt.append("사진 스타일: ").append(conditions.getPhotoStyle()).append("\n");
+ }
+ if (conditions.getRequirement() != null) {
+ prompt.append("요구사항: ").append(conditions.getRequirement()).append("\n");
+ }
+ if (conditions.getToneAndManner() != null) {
+ prompt.append("톤앤매너: ").append(conditions.getToneAndManner()).append("\n");
+ }
+
+ return prompt.toString();
+ }
+
+ /**
+ * 더미 포스터 URL 생성 (개발용)
+ */
+ private String generateDummyPosterUrl(String title) {
+ return String.format("https://example.com/posters/%s-poster.jpg",
+ title.replaceAll("\\s+", "-").toLowerCase());
+ }
+
+ /**
+ * 더미 포스터 사이즈별 URL 생성 (개발용)
+ */
+ private Map generateDummyPosterSizes(String originalImage) {
+ Map sizes = new HashMap<>();
+ String baseUrl = originalImage.substring(0, originalImage.lastIndexOf("."));
+ String extension = originalImage.substring(originalImage.lastIndexOf("."));
+
+ sizes.put("small", baseUrl + "-small" + extension);
+ sizes.put("medium", baseUrl + "-medium" + extension);
+ sizes.put("large", baseUrl + "-large" + extension);
+ sizes.put("xlarge", baseUrl + "-xlarge" + extension);
+
+ return sizes;
+ }
+
+ /**
+ * 폴백 포스터 URL 생성 (AI 서비스 실패 시)
+ */
+ private String generateFallbackPosterUrl() {
+ return "https://example.com/posters/default-poster.jpg";
+ }
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/mapper/ContentMapper.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/mapper/ContentMapper.java
index 49cc6b4..a03954f 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/mapper/ContentMapper.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/mapper/ContentMapper.java
@@ -91,7 +91,7 @@ public class ContentMapper {
entity.getConditions().getStartDate(),
entity.getConditions().getEndDate(),
entity.getConditions().getPhotoStyle(),
- entity.getConditions().getTargetAudience(),
+ // entity.getConditions().getTargetAudience(),
entity.getConditions().getPromotionType()
);
}
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/JpaContentRepository.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/JpaContentRepository.java
index 9396d4d..da461e5 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/JpaContentRepository.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/JpaContentRepository.java
@@ -1,3 +1,4 @@
+// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/JpaContentRepository.java
package com.won.smarketing.content.infrastructure.repository;
import com.won.smarketing.content.domain.model.Content;
@@ -5,60 +6,44 @@ import com.won.smarketing.content.domain.model.ContentId;
import com.won.smarketing.content.domain.model.ContentType;
import com.won.smarketing.content.domain.model.Platform;
import com.won.smarketing.content.domain.repository.ContentRepository;
-import com.won.smarketing.content.infrastructure.entity.ContentJpaEntity;
-import com.won.smarketing.content.infrastructure.mapper.ContentMapper;
import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
-import java.util.stream.Collectors;
/**
- * JPA 기반 콘텐츠 Repository 구현체
- *
- * @author smarketing-team
- * @version 1.0
+ * JPA를 활용한 콘텐츠 리포지토리 구현체
+ * Clean Architecture의 Infrastructure Layer에 위치
*/
@Repository
@RequiredArgsConstructor
-@Slf4j
public class JpaContentRepository implements ContentRepository {
- private final SpringDataContentRepository springDataContentRepository;
- private final ContentMapper contentMapper;
+ private final JpaContentRepositoryInterface jpaRepository;
/**
- * 콘텐츠를 저장합니다.
- *
+ * 콘텐츠 저장
* @param content 저장할 콘텐츠
* @return 저장된 콘텐츠
*/
@Override
public Content save(Content content) {
- log.debug("Saving content: {}", content.getId());
- ContentJpaEntity entity = contentMapper.toEntity(content);
- ContentJpaEntity savedEntity = springDataContentRepository.save(entity);
- return contentMapper.toDomain(savedEntity);
+ return jpaRepository.save(content);
}
/**
- * ID로 콘텐츠를 조회합니다.
- *
+ * ID로 콘텐츠 조회
* @param id 콘텐츠 ID
* @return 조회된 콘텐츠
*/
@Override
public Optional findById(ContentId id) {
- log.debug("Finding content by id: {}", id.getValue());
- return springDataContentRepository.findById(id.getValue())
- .map(contentMapper::toDomain);
+ return jpaRepository.findById(id.getValue());
}
/**
- * 필터 조건으로 콘텐츠 목록을 조회합니다.
- *
+ * 필터 조건으로 콘텐츠 목록 조회
* @param contentType 콘텐츠 타입
* @param platform 플랫폼
* @param period 기간
@@ -67,45 +52,25 @@ public class JpaContentRepository implements ContentRepository {
*/
@Override
public List findByFilters(ContentType contentType, Platform platform, String period, String sortBy) {
- log.debug("Finding contents by filters - type: {}, platform: {}, period: {}, sortBy: {}",
- contentType, platform, period, sortBy);
-
- List entities = springDataContentRepository.findByFilters(
- contentType != null ? contentType.name() : null,
- platform != null ? platform.name() : null,
- period,
- sortBy
- );
-
- return entities.stream()
- .map(contentMapper::toDomain)
- .collect(Collectors.toList());
+ return jpaRepository.findByFilters(contentType, platform, period, sortBy);
}
/**
- * 진행 중인 콘텐츠 목록을 조회합니다.
- *
+ * 진행 중인 콘텐츠 목록 조회
* @param period 기간
* @return 진행 중인 콘텐츠 목록
*/
@Override
public List findOngoingContents(String period) {
- log.debug("Finding ongoing contents for period: {}", period);
- List entities = springDataContentRepository.findOngoingContents(period);
-
- return entities.stream()
- .map(contentMapper::toDomain)
- .collect(Collectors.toList());
+ return jpaRepository.findOngoingContents(period);
}
/**
- * ID로 콘텐츠를 삭제합니다.
- *
- * @param id 콘텐츠 ID
+ * ID로 콘텐츠 삭제
+ * @param id 삭제할 콘텐츠 ID
*/
@Override
public void deleteById(ContentId id) {
- log.debug("Deleting content by id: {}", id.getValue());
- springDataContentRepository.deleteById(id.getValue());
+ jpaRepository.deleteById(id.getValue());
}
}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/JpaContentRepositoryInterface.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/JpaContentRepositoryInterface.java
new file mode 100644
index 0000000..380bba6
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/JpaContentRepositoryInterface.java
@@ -0,0 +1,49 @@
+// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/JpaContentRepositoryInterface.java
+package com.won.smarketing.content.infrastructure.repository;
+
+import com.won.smarketing.content.domain.model.Content;
+import com.won.smarketing.content.domain.model.ContentType;
+import com.won.smarketing.content.domain.model.Platform;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.List;
+
+/**
+ * Spring Data JPA 콘텐츠 리포지토리 인터페이스
+ * Clean Architecture의 Infrastructure Layer에 위치
+ */
+public interface JpaContentRepositoryInterface extends JpaRepository {
+
+ /**
+ * 필터 조건으로 콘텐츠 목록 조회
+ */
+ @Query("SELECT c FROM Content c WHERE " +
+ "(:contentType IS NULL OR c.contentType = :contentType) AND " +
+ "(:platform IS NULL OR c.platform = :platform) AND " +
+ "(:period IS NULL OR " +
+ " (:period = 'week' AND c.createdAt >= CURRENT_DATE - 7) OR " +
+ " (:period = 'month' AND c.createdAt >= CURRENT_DATE - 30) OR " +
+ " (:period = 'year' AND c.createdAt >= CURRENT_DATE - 365)) " +
+ "ORDER BY " +
+ "CASE WHEN :sortBy = 'latest' THEN c.createdAt END DESC, " +
+ "CASE WHEN :sortBy = 'oldest' THEN c.createdAt END ASC, " +
+ "CASE WHEN :sortBy = 'title' THEN c.title END ASC")
+ List findByFilters(@Param("contentType") ContentType contentType,
+ @Param("platform") Platform platform,
+ @Param("period") String period,
+ @Param("sortBy") String sortBy);
+
+ /**
+ * 진행 중인 콘텐츠 목록 조회
+ */
+ @Query("SELECT c FROM Content c WHERE " +
+ "c.status IN ('PUBLISHED', 'SCHEDULED') AND " +
+ "(:period IS NULL OR " +
+ " (:period = 'week' AND c.createdAt >= CURRENT_DATE - 7) OR " +
+ " (:period = 'month' AND c.createdAt >= CURRENT_DATE - 30) OR " +
+ " (:period = 'year' AND c.createdAt >= CURRENT_DATE - 365)) " +
+ "ORDER BY c.createdAt DESC")
+ List findOngoingContents(@Param("period") String period);
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/CreationConditionsDto.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/CreationConditionsDto.java
new file mode 100644
index 0000000..403cdfa
--- /dev/null
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/CreationConditionsDto.java
@@ -0,0 +1,45 @@
+// marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/CreationConditionsDto.java
+package com.won.smarketing.content.presentation.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDate;
+
+/**
+ * 콘텐츠 생성 조건 DTO
+ */
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@Schema(description = "콘텐츠 생성 조건")
+public class CreationConditionsDto {
+
+ @Schema(description = "카테고리", example = "음식")
+ private String category;
+
+ @Schema(description = "생성 요구사항", example = "젊은 고객층을 타겟으로 한 재미있는 콘텐츠")
+ private String requirement;
+
+ @Schema(description = "톤앤매너", example = "친근하고 활발한")
+ private String toneAndManner;
+
+ @Schema(description = "감정 강도", example = "보통")
+ private String emotionIntensity;
+
+ @Schema(description = "이벤트명", example = "신메뉴 출시 이벤트")
+ private String eventName;
+
+ @Schema(description = "시작일")
+ private LocalDate startDate;
+
+ @Schema(description = "종료일")
+ private LocalDate endDate;
+
+ @Schema(description = "사진 스타일", example = "모던하고 깔끔한")
+ private String photoStyle;
+}
\ No newline at end of file
diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateResponse.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateResponse.java
index ce5ee97..0acf9ec 100644
--- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateResponse.java
+++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateResponse.java
@@ -306,7 +306,7 @@ public class SnsContentCreateResponse {
// 생성 조건 정보 설정
if (content.getCreationConditions() != null) {
builder.generationConditions(GenerationConditionsDto.builder()
- .targetAudience(content.getCreationConditions().getTargetAudience())
+ //.targetAudience(content.getCreationConditions().getTargetAudience())
.eventName(content.getCreationConditions().getEventName())
.toneAndManner(content.getCreationConditions().getToneAndManner())
.promotionType(content.getCreationConditions().getPromotionType())
diff --git a/smarketing-java/marketing-content/src/main/resources/application.yml b/smarketing-java/marketing-content/src/main/resources/application.yml
index 9f7259f..0e9e68c 100644
--- a/smarketing-java/marketing-content/src/main/resources/application.yml
+++ b/smarketing-java/marketing-content/src/main/resources/application.yml
@@ -11,8 +11,8 @@ spring:
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
- ddl-auto: ${JPA_DDL_AUTO:update}
- show-sql: ${JPA_SHOW_SQL:true}
+ ddl-auto: ${DDL_AUTO:update}
+ show-sql: ${SHOW_SQL:true}
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect