diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/MarketingContentServiceApplication.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/MarketingContentServiceApplication.java index 50d69a1..537a189 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/MarketingContentServiceApplication.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/MarketingContentServiceApplication.java @@ -3,6 +3,7 @@ package com.won.smarketing.content; 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.EnableJpaAuditing; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; /** @@ -17,8 +18,9 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories; "com.won.smarketing.content.infrastructure.repository" }) @EntityScan(basePackages = { - "com.won.smarketing.content.domain.model" + "com.won.smarketing.content.infrastructure.entity" }) +@EnableJpaAuditing public class MarketingContentServiceApplication { public static void main(String[] args) { diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/ContentConfig.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/ContentConfig.java new file mode 100644 index 0000000..3931d19 --- /dev/null +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/ContentConfig.java @@ -0,0 +1,9 @@ +package com.won.smarketing.content.config; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan(basePackages = "com.won.smarketing.content") +public class ContentConfig { +} \ 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 index 5cf42a4..9d72f1f 100644 --- 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 @@ -1,8 +1,10 @@ // marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java package com.won.smarketing.content.infrastructure.external; +// 수정: domain 패키지의 인터페이스를 import +import com.won.smarketing.content.domain.service.AiContentGenerator; import com.won.smarketing.content.domain.model.Platform; -import com.won.smarketing.content.domain.model.CreationConditions; +import com.won.smarketing.content.presentation.dto.SnsContentCreateRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -21,105 +23,73 @@ 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) { + public String generateSnsContent(SnsContentCreateRequest request) { try { - // Claude AI API 호출 로직 (실제 구현에서는 HTTP 클라이언트를 사용) - String prompt = buildContentPrompt(title, category, platform, conditions); - - // TODO: 실제 Claude AI API 호출 - // 현재는 더미 데이터 반환 - return generateDummySnsContent(title, platform); - + String prompt = buildContentPrompt(request); + return generateDummySnsContent(request.getTitle(), Platform.fromString(request.getPlatform())); } catch (Exception e) { log.error("AI 콘텐츠 생성 실패: {}", e.getMessage(), e); - return generateFallbackContent(title, platform); + return generateFallbackContent(request.getTitle(), Platform.fromString(request.getPlatform())); } } /** - * 해시태그 생성 - * 콘텐츠 내용을 분석하여 관련 해시태그를 생성합니다. - * - * @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("#맛집", "#신메뉴", "#추천"); + return generateFallbackHashtags(); } } - /** - * AI 프롬프트 생성 - */ - private String buildContentPrompt(String title, String category, Platform platform, CreationConditions conditions) { + private String buildContentPrompt(SnsContentCreateRequest request) { StringBuilder prompt = new StringBuilder(); - prompt.append("다음 조건에 맞는 ").append(platform.getDisplayName()).append(" 게시물을 작성해주세요:\n"); - prompt.append("제목: ").append(title).append("\n"); - prompt.append("카테고리: ").append(category).append("\n"); + prompt.append("제목: ").append(request.getTitle()).append("\n"); + prompt.append("카테고리: ").append(request.getCategory()).append("\n"); + prompt.append("플랫폼: ").append(request.getPlatform()).append("\n"); - if (conditions.getRequirement() != null) { - prompt.append("요구사항: ").append(conditions.getRequirement()).append("\n"); + if (request.getRequirement() != null) { + prompt.append("요구사항: ").append(request.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"); + + if (request.getToneAndManner() != null) { + prompt.append("톤앤매너: ").append(request.getToneAndManner()).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); + String baseContent = "🌟 " + title + "를 소개합니다! 🌟\n\n" + + "저희 매장에서 특별한 경험을 만나보세요.\n" + + "고객 여러분의 소중한 시간을 더욱 특별하게 만들어드리겠습니다.\n\n"; + + if (platform == Platform.INSTAGRAM) { + return baseContent + "더 많은 정보는 프로필 링크에서 확인하세요! 📸"; + } else { + return baseContent + "자세한 내용은 저희 블로그를 방문해 주세요! ✨"; } } - /** - * 더미 해시태그 생성 (개발용) - */ - 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); + return title + "에 대한 멋진 콘텐츠입니다. 많은 관심 부탁드립니다!"; + } + + private List generateDummyHashtags(Platform platform) { + if (platform == Platform.INSTAGRAM) { + return Arrays.asList("#맛집", "#데일리", "#소상공인", "#추천", "#인스타그램"); + } else { + return Arrays.asList("#맛집추천", "#블로그", "#리뷰", "#맛있는곳", "#소상공인응원"); + } + } + + private List generateFallbackHashtags() { + return Arrays.asList("#소상공인", "#마케팅", "#홍보"); } } \ 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 index a667545..7495966 100644 --- 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 @@ -1,7 +1,8 @@ // 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 com.won.smarketing.content.domain.service.AiPosterGenerator; // 도메인 인터페이스 import +import com.won.smarketing.content.presentation.dto.PosterContentCreateRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -19,23 +20,20 @@ import java.util.Map; public class ClaudeAiPosterGenerator implements AiPosterGenerator { /** - * 포스터 이미지 생성 - * Claude AI API를 호출하여 홍보 포스터를 생성합니다. + * 포스터 생성 * - * @param title 제목 - * @param category 카테고리 - * @param conditions 생성 조건 + * @param request 포스터 생성 요청 * @return 생성된 포스터 이미지 URL */ @Override - public String generatePoster(String title, String category, CreationConditions conditions) { + public String generatePoster(PosterContentCreateRequest request) { try { - // Claude AI API 호출 로직 (실제 구현에서는 HTTP 클라이언트를 사용) - String prompt = buildPosterPrompt(title, category, conditions); + // Claude AI API 호출 로직 + String prompt = buildPosterPrompt(request); // TODO: 실제 Claude AI API 호출 // 현재는 더미 데이터 반환 - return generateDummyPosterUrl(title); + return generateDummyPosterUrl(request.getTitle()); } catch (Exception e) { log.error("AI 포스터 생성 실패: {}", e.getMessage(), e); @@ -44,75 +42,45 @@ public class ClaudeAiPosterGenerator implements AiPosterGenerator { } /** - * 포스터 다양한 사이즈 생성 - * 원본 포스터를 기반으로 다양한 사이즈의 포스터를 생성합니다. + * 다양한 사이즈의 포스터 생성 * - * @param originalImage 원본 이미지 URL - * @return 사이즈별 이미지 URL 맵 + * @param baseImage 기본 이미지 + * @return 사이즈별 포스터 URL 맵 */ @Override - public Map generatePosterSizes(String originalImage) { - try { - // TODO: 실제 이미지 리사이징 API 호출 - // 현재는 더미 데이터 반환 - return generateDummyPosterSizes(originalImage); + public Map generatePosterSizes(String baseImage) { + Map sizes = new HashMap<>(); - } catch (Exception e) { - log.error("포스터 사이즈 생성 실패: {}", e.getMessage(), e); - return new HashMap<>(); - } + // 다양한 사이즈 생성 (더미 구현) + sizes.put("instagram_square", baseImage + "_1080x1080.jpg"); + sizes.put("instagram_story", baseImage + "_1080x1920.jpg"); + sizes.put("facebook_post", baseImage + "_1200x630.jpg"); + sizes.put("a4_poster", baseImage + "_2480x3508.jpg"); + + return sizes; } - /** - * AI 포스터 프롬프트 생성 - */ - private String buildPosterPrompt(String title, String category, CreationConditions conditions) { + private String buildPosterPrompt(PosterContentCreateRequest request) { StringBuilder prompt = new StringBuilder(); - prompt.append("다음 조건에 맞는 홍보 포스터를 생성해주세요:\n"); - prompt.append("제목: ").append(title).append("\n"); - prompt.append("카테고리: ").append(category).append("\n"); + prompt.append("포스터 제목: ").append(request.getTitle()).append("\n"); + prompt.append("카테고리: ").append(request.getCategory()).append("\n"); - if (conditions.getPhotoStyle() != null) { - prompt.append("사진 스타일: ").append(conditions.getPhotoStyle()).append("\n"); + if (request.getRequirement() != null) { + prompt.append("요구사항: ").append(request.getRequirement()).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 (request.getToneAndManner() != null) { + prompt.append("톤앤매너: ").append(request.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()); + return "https://dummy-ai-service.com/posters/" + title.hashCode() + ".jpg"; } - /** - * 더미 포스터 사이즈별 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"; + return "https://dummy-ai-service.com/posters/fallback.jpg"; } } \ No newline at end of file diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/SpringDataContentRepository.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/SpringDataContentRepository.java deleted file mode 100644 index feba6b4..0000000 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/repository/SpringDataContentRepository.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.won.smarketing.content.infrastructure.repository; - -import com.won.smarketing.content.infrastructure.entity.ContentJpaEntity; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.util.List; - -/** - * Spring Data JPA 콘텐츠 Repository - * - * @author smarketing-team - * @version 1.0 - */ -@Repository -public interface SpringDataContentRepository extends JpaRepository { - - /** - * 필터 조건으로 콘텐츠를 조회합니다. - * - * @param contentType 콘텐츠 타입 - * @param platform 플랫폼 - * @param period 기간 - * @param sortBy 정렬 기준 - * @return 콘텐츠 목록 - */ - @Query("SELECT c FROM ContentJpaEntity c WHERE " + - "(:contentType IS NULL OR c.contentType = :contentType) AND " + - "(:platform IS NULL OR c.platform = :platform) AND " + - "(:period IS NULL OR DATE(c.createdAt) >= CURRENT_DATE - INTERVAL :period DAY) " + - "ORDER BY " + - "CASE WHEN :sortBy = 'latest' THEN c.createdAt END DESC, " + - "CASE WHEN :sortBy = 'oldest' THEN c.createdAt END ASC") - List findByFilters(@Param("contentType") String contentType, - @Param("platform") String platform, - @Param("period") String period, - @Param("sortBy") String sortBy); - - /** - * 진행 중인 콘텐츠를 조회합니다. - * - * @param period 기간 - * @return 진행 중인 콘텐츠 목록 - */ - @Query("SELECT c FROM ContentJpaEntity c " + - "WHERE c.status = 'PUBLISHED' AND " + - "(:period IS NULL OR DATE(c.createdAt) >= CURRENT_DATE - INTERVAL :period DAY)") - List findOngoingContents(@Param("period") String period); -} \ No newline at end of file diff --git a/smarketing-java/marketing-content/src/main/resources/application.yml b/smarketing-java/marketing-content/src/main/resources/application.yml index 0e9e68c..10dc73d 100644 --- a/smarketing-java/marketing-content/src/main/resources/application.yml +++ b/smarketing-java/marketing-content/src/main/resources/application.yml @@ -17,21 +17,17 @@ spring: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect format_sql: true -ai: - service: - url: ${AI_SERVICE_URL:http://localhost:8080/ai} - timeout: ${AI_SERVICE_TIMEOUT:30000} + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASSWORD:} -external: - claude-ai: - api-key: ${CLAUDE_AI_API_KEY:your-claude-api-key} - base-url: ${CLAUDE_AI_BASE_URL:https://api.anthropic.com} - model: ${CLAUDE_AI_MODEL:claude-3-sonnet-20240229} - max-tokens: ${CLAUDE_AI_MAX_TOKENS:4000} jwt: secret: ${JWT_SECRET:mySecretKeyForJWTTokenGenerationAndValidation123456789} access-token-validity: ${JWT_ACCESS_VALIDITY:3600000} refresh-token-validity: ${JWT_REFRESH_VALIDITY:604800000} + logging: level: - com.won.smarketing.content: ${LOG_LEVEL:DEBUG} + com.won.smarketing: ${LOG_LEVEL:DEBUG}