fix marketing-content

This commit is contained in:
박서은 2025-06-12 11:02:07 +09:00
parent d265bb2065
commit de6b160aca
6 changed files with 88 additions and 194 deletions

View File

@ -3,6 +3,7 @@ package com.won.smarketing.content;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 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" "com.won.smarketing.content.infrastructure.repository"
}) })
@EntityScan(basePackages = { @EntityScan(basePackages = {
"com.won.smarketing.content.domain.model" "com.won.smarketing.content.infrastructure.entity"
}) })
@EnableJpaAuditing
public class MarketingContentServiceApplication { public class MarketingContentServiceApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@ -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 {
}

View File

@ -1,8 +1,10 @@
// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java // marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java
package com.won.smarketing.content.infrastructure.external; 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.Platform;
import com.won.smarketing.content.domain.model.CreationConditions; import com.won.smarketing.content.presentation.dto.SnsContentCreateRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -21,105 +23,73 @@ public class ClaudeAiContentGenerator implements AiContentGenerator {
/** /**
* SNS 콘텐츠 생성 * SNS 콘텐츠 생성
* Claude AI API를 호출하여 SNS 게시물을 생성합니다.
*
* @param title 제목
* @param category 카테고리
* @param platform 플랫폼
* @param conditions 생성 조건
* @return 생성된 콘텐츠 텍스트
*/ */
@Override @Override
public String generateSnsContent(String title, String category, Platform platform, CreationConditions conditions) { public String generateSnsContent(SnsContentCreateRequest request) {
try { try {
// Claude AI API 호출 로직 (실제 구현에서는 HTTP 클라이언트를 사용) String prompt = buildContentPrompt(request);
String prompt = buildContentPrompt(title, category, platform, conditions); return generateDummySnsContent(request.getTitle(), Platform.fromString(request.getPlatform()));
// TODO: 실제 Claude AI API 호출
// 현재는 더미 데이터 반환
return generateDummySnsContent(title, platform);
} catch (Exception e) { } catch (Exception e) {
log.error("AI 콘텐츠 생성 실패: {}", e.getMessage(), 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 @Override
public List<String> generateHashtags(String content, Platform platform) { public List<String> generateHashtags(String content, Platform platform) {
try { try {
// TODO: 실제 Claude AI API 호출하여 해시태그 생성
// 현재는 더미 데이터 반환
return generateDummyHashtags(platform); return generateDummyHashtags(platform);
} catch (Exception e) { } catch (Exception e) {
log.error("해시태그 생성 실패: {}", e.getMessage(), e); log.error("해시태그 생성 실패: {}", e.getMessage(), e);
return Arrays.asList("#맛집", "#신메뉴", "#추천"); return generateFallbackHashtags();
} }
} }
/** private String buildContentPrompt(SnsContentCreateRequest request) {
* AI 프롬프트 생성
*/
private String buildContentPrompt(String title, String category, Platform platform, CreationConditions conditions) {
StringBuilder prompt = new StringBuilder(); StringBuilder prompt = new StringBuilder();
prompt.append("다음 조건에 맞는 ").append(platform.getDisplayName()).append(" 게시물을 작성해주세요:\n"); prompt.append("제목: ").append(request.getTitle()).append("\n");
prompt.append("제목: ").append(title).append("\n"); prompt.append("카테고리: ").append(request.getCategory()).append("\n");
prompt.append("카테고리: ").append(category).append("\n"); prompt.append("플랫폼: ").append(request.getPlatform()).append("\n");
if (conditions.getRequirement() != null) { if (request.getRequirement() != null) {
prompt.append("요구사항: ").append(conditions.getRequirement()).append("\n"); prompt.append("요구사항: ").append(request.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");
if (conditions.getEmotionIntensity() != null) {
prompt.append("감정 강도: ").append(conditions.getEmotionIntensity()).append("\n");
} }
return prompt.toString(); return prompt.toString();
} }
/**
* 더미 SNS 콘텐츠 생성 (개발용)
*/
private String generateDummySnsContent(String title, Platform platform) { private String generateDummySnsContent(String title, Platform platform) {
switch (platform) { String baseContent = "🌟 " + title + "를 소개합니다! 🌟\n\n" +
case INSTAGRAM: "저희 매장에서 특별한 경험을 만나보세요.\n" +
return String.format("🎉 %s\n\n맛있는 순간을 놓치지 마세요! 새로운 맛의 경험이 여러분을 기다리고 있어요. 따뜻한 분위기에서 즐기는 특별한 시간을 만들어보세요.\n\n📍 지금 바로 방문해보세요!", title); "고객 여러분의 소중한 시간을 더욱 특별하게 만들어드리겠습니다.\n\n";
case NAVER_BLOG:
return String.format("안녕하세요! 오늘은 %s에 대해 소개해드리려고 해요.\n\n정성스럽게 준비한 새로운 메뉴로 고객 여러분께 더 나은 경험을 선사하고 싶습니다. 많은 관심과 사랑 부탁드려요!", title); if (platform == Platform.INSTAGRAM) {
default: return baseContent + "더 많은 정보는 프로필 링크에서 확인하세요! 📸";
return String.format("%s - 새로운 경험을 만나보세요!", title); } else {
return baseContent + "자세한 내용은 저희 블로그를 방문해 주세요! ✨";
} }
} }
/**
* 더미 해시태그 생성 (개발용)
*/
private List<String> 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) { private String generateFallbackContent(String title, Platform platform) {
return String.format("🎉 %s\n\n새로운 소식을 전해드립니다. 많은 관심 부탁드려요!", title); return title + "에 대한 멋진 콘텐츠입니다. 많은 관심 부탁드립니다!";
}
private List<String> generateDummyHashtags(Platform platform) {
if (platform == Platform.INSTAGRAM) {
return Arrays.asList("#맛집", "#데일리", "#소상공인", "#추천", "#인스타그램");
} else {
return Arrays.asList("#맛집추천", "#블로그", "#리뷰", "#맛있는곳", "#소상공인응원");
}
}
private List<String> generateFallbackHashtags() {
return Arrays.asList("#소상공인", "#마케팅", "#홍보");
} }
} }

View File

@ -1,7 +1,8 @@
// marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiPosterGenerator.java // marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiPosterGenerator.java
package com.won.smarketing.content.infrastructure.external; 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.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -19,23 +20,20 @@ import java.util.Map;
public class ClaudeAiPosterGenerator implements AiPosterGenerator { public class ClaudeAiPosterGenerator implements AiPosterGenerator {
/** /**
* 포스터 이미지 생성 * 포스터 생성
* Claude AI API를 호출하여 홍보 포스터를 생성합니다.
* *
* @param title 제목 * @param request 포스터 생성 요청
* @param category 카테고리
* @param conditions 생성 조건
* @return 생성된 포스터 이미지 URL * @return 생성된 포스터 이미지 URL
*/ */
@Override @Override
public String generatePoster(String title, String category, CreationConditions conditions) { public String generatePoster(PosterContentCreateRequest request) {
try { try {
// Claude AI API 호출 로직 (실제 구현에서는 HTTP 클라이언트를 사용) // Claude AI API 호출 로직
String prompt = buildPosterPrompt(title, category, conditions); String prompt = buildPosterPrompt(request);
// TODO: 실제 Claude AI API 호출 // TODO: 실제 Claude AI API 호출
// 현재는 더미 데이터 반환 // 현재는 더미 데이터 반환
return generateDummyPosterUrl(title); return generateDummyPosterUrl(request.getTitle());
} catch (Exception e) { } catch (Exception e) {
log.error("AI 포스터 생성 실패: {}", e.getMessage(), e); log.error("AI 포스터 생성 실패: {}", e.getMessage(), e);
@ -44,75 +42,45 @@ public class ClaudeAiPosterGenerator implements AiPosterGenerator {
} }
/** /**
* 포스터 다양한 사이즈 생성 * 다양한 사이즈의 포스터 생성
* 원본 포스터를 기반으로 다양한 사이즈의 포스터를 생성합니다.
* *
* @param originalImage 원본 이미지 URL * @param baseImage 기본 이미지
* @return 사이즈별 이미지 URL * @return 사이즈별 포스터 URL
*/ */
@Override @Override
public Map<String, String> generatePosterSizes(String originalImage) { public Map<String, String> generatePosterSizes(String baseImage) {
try { Map<String, String> sizes = new HashMap<>();
// TODO: 실제 이미지 리사이징 API 호출
// 현재는 더미 데이터 반환
return generateDummyPosterSizes(originalImage);
} catch (Exception e) { // 다양한 사이즈 생성 (더미 구현)
log.error("포스터 사이즈 생성 실패: {}", e.getMessage(), e); sizes.put("instagram_square", baseImage + "_1080x1080.jpg");
return new HashMap<>(); sizes.put("instagram_story", baseImage + "_1080x1920.jpg");
} sizes.put("facebook_post", baseImage + "_1200x630.jpg");
sizes.put("a4_poster", baseImage + "_2480x3508.jpg");
return sizes;
} }
/** private String buildPosterPrompt(PosterContentCreateRequest request) {
* AI 포스터 프롬프트 생성
*/
private String buildPosterPrompt(String title, String category, CreationConditions conditions) {
StringBuilder prompt = new StringBuilder(); StringBuilder prompt = new StringBuilder();
prompt.append("다음 조건에 맞는 홍보 포스터를 생성해주세요:\n"); prompt.append("포스터 제목: ").append(request.getTitle()).append("\n");
prompt.append("제목: ").append(title).append("\n"); prompt.append("카테고리: ").append(request.getCategory()).append("\n");
prompt.append("카테고리: ").append(category).append("\n");
if (conditions.getPhotoStyle() != null) { if (request.getRequirement() != null) {
prompt.append("사진 스타일: ").append(conditions.getPhotoStyle()).append("\n"); prompt.append("요구사항: ").append(request.getRequirement()).append("\n");
} }
if (conditions.getRequirement() != null) {
prompt.append("요구사항: ").append(conditions.getRequirement()).append("\n"); if (request.getToneAndManner() != null) {
} prompt.append("톤앤매너: ").append(request.getToneAndManner()).append("\n");
if (conditions.getToneAndManner() != null) {
prompt.append("톤앤매너: ").append(conditions.getToneAndManner()).append("\n");
} }
return prompt.toString(); return prompt.toString();
} }
/**
* 더미 포스터 URL 생성 (개발용)
*/
private String generateDummyPosterUrl(String title) { private String generateDummyPosterUrl(String title) {
return String.format("https://example.com/posters/%s-poster.jpg", return "https://dummy-ai-service.com/posters/" + title.hashCode() + ".jpg";
title.replaceAll("\\s+", "-").toLowerCase());
} }
/**
* 더미 포스터 사이즈별 URL 생성 (개발용)
*/
private Map<String, String> generateDummyPosterSizes(String originalImage) {
Map<String, String> 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() { private String generateFallbackPosterUrl() {
return "https://example.com/posters/default-poster.jpg"; return "https://dummy-ai-service.com/posters/fallback.jpg";
} }
} }

View File

@ -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<ContentJpaEntity, Long> {
/**
* 필터 조건으로 콘텐츠를 조회합니다.
*
* @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<ContentJpaEntity> 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<ContentJpaEntity> findOngoingContents(@Param("period") String period);
}

View File

@ -17,21 +17,17 @@ spring:
hibernate: hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true format_sql: true
ai: data:
service: redis:
url: ${AI_SERVICE_URL:http://localhost:8080/ai} host: ${REDIS_HOST:localhost}
timeout: ${AI_SERVICE_TIMEOUT:30000} 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: jwt:
secret: ${JWT_SECRET:mySecretKeyForJWTTokenGenerationAndValidation123456789} secret: ${JWT_SECRET:mySecretKeyForJWTTokenGenerationAndValidation123456789}
access-token-validity: ${JWT_ACCESS_VALIDITY:3600000} access-token-validity: ${JWT_ACCESS_VALIDITY:3600000}
refresh-token-validity: ${JWT_REFRESH_VALIDITY:604800000} refresh-token-validity: ${JWT_REFRESH_VALIDITY:604800000}
logging: logging:
level: level:
com.won.smarketing.content: ${LOG_LEVEL:DEBUG} com.won.smarketing: ${LOG_LEVEL:DEBUG}