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 + + + + \ 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