mirror of
https://github.com/won-ktds/smarketing-backend.git
synced 2025-12-06 07:06:24 +00:00
add python call
This commit is contained in:
parent
3e09e77707
commit
faaf690db2
@ -1,4 +1,7 @@
|
||||
dependencies {
|
||||
implementation project(':common')
|
||||
runtimeOnly 'org.postgresql:postgresql'
|
||||
|
||||
// WebClient를 위한 Spring WebFlux 의존성
|
||||
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
||||
}
|
||||
@ -99,24 +99,24 @@ public class ContentQueryService implements ContentQueryUseCase {
|
||||
* @param contentId 콘텐츠 ID
|
||||
* @return 콘텐츠 상세 정보
|
||||
*/
|
||||
@Override
|
||||
public ContentDetailResponse getContentDetail(Long contentId) {
|
||||
Content content = contentRepository.findById(ContentId.of(contentId))
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.CONTENT_NOT_FOUND));
|
||||
|
||||
return ContentDetailResponse.builder()
|
||||
.contentId(content.getId())
|
||||
.contentType(content.getContentType().name())
|
||||
.platform(content.getPlatform().name())
|
||||
.title(content.getTitle())
|
||||
.content(content.getContent())
|
||||
.hashtags(content.getHashtags())
|
||||
.images(content.getImages())
|
||||
.status(content.getStatus().name())
|
||||
.creationConditions(toCreationConditionsDto(content.getCreationConditions()))
|
||||
.createdAt(content.getCreatedAt())
|
||||
.build();
|
||||
}
|
||||
// @Override
|
||||
// public ContentDetailResponse getContentDetail(Long contentId) {
|
||||
// Content content = contentRepository.findById(ContentId.of(contentId))
|
||||
// .orElseThrow(() -> new BusinessException(ErrorCode.CONTENT_NOT_FOUND));
|
||||
//
|
||||
// return ContentDetailResponse.builder()
|
||||
// .contentId(content.getId())
|
||||
// .contentType(content.getContentType().name())
|
||||
// .platform(content.getPlatform().name())
|
||||
// .title(content.getTitle())
|
||||
// .content(content.getContent())
|
||||
// .hashtags(content.getHashtags())
|
||||
// .images(content.getImages())
|
||||
// .status(content.getStatus().name())
|
||||
// .creationConditions(toCreationConditionsDto(content.getCreationConditions()))
|
||||
// .createdAt(content.getCreatedAt())
|
||||
// .build();
|
||||
// }
|
||||
|
||||
/**
|
||||
* 콘텐츠 삭제
|
||||
@ -177,15 +177,15 @@ public class ContentQueryService implements ContentQueryUseCase {
|
||||
* @param conditions CreationConditions 도메인 객체
|
||||
* @return CreationConditionsDto
|
||||
*/
|
||||
private ContentDetailResponse.CreationConditionsDto toCreationConditionsDto(CreationConditions conditions) {
|
||||
if (conditions == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ContentDetailResponse.CreationConditionsDto.builder()
|
||||
.toneAndManner(conditions.getToneAndManner())
|
||||
.emotionIntensity(conditions.getEmotionIntensity())
|
||||
.eventName(conditions.getEventName())
|
||||
.build();
|
||||
}
|
||||
// private ContentDetailResponse.CreationConditionsDto toCreationConditionsDto(CreationConditions conditions) {
|
||||
// if (conditions == null) {
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// return ContentDetailResponse.CreationConditionsDto.builder()
|
||||
// .toneAndManner(conditions.getToneAndManner())
|
||||
// .emotionIntensity(conditions.getEmotionIntensity())
|
||||
// .eventName(conditions.getEventName())
|
||||
// .build();
|
||||
// }
|
||||
}
|
||||
|
||||
@ -49,8 +49,8 @@ public class PosterContentService implements PosterContentUseCase {
|
||||
CreationConditions conditions = CreationConditions.builder()
|
||||
.category(request.getCategory())
|
||||
.requirement(request.getRequirement())
|
||||
.toneAndManner(request.getToneAndManner())
|
||||
.emotionIntensity(request.getEmotionIntensity())
|
||||
// .toneAndManner(request.getToneAndManner())
|
||||
// .emotionIntensity(request.getEmotionIntensity())
|
||||
.eventName(request.getEventName())
|
||||
.startDate(request.getStartDate())
|
||||
.endDate(request.getEndDate())
|
||||
@ -80,8 +80,8 @@ public class PosterContentService implements PosterContentUseCase {
|
||||
CreationConditions conditions = CreationConditions.builder()
|
||||
.category(request.getCategory())
|
||||
.requirement(request.getRequirement())
|
||||
.toneAndManner(request.getToneAndManner())
|
||||
.emotionIntensity(request.getEmotionIntensity())
|
||||
// .toneAndManner(request.getToneAndManner())
|
||||
// .emotionIntensity(request.getEmotionIntensity())
|
||||
.eventName(request.getEventName())
|
||||
.startDate(request.getStartDate())
|
||||
.endDate(request.getEndDate())
|
||||
|
||||
@ -41,49 +41,12 @@ public class SnsContentService implements SnsContentUseCase {
|
||||
@Transactional
|
||||
public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request) {
|
||||
// AI를 사용하여 SNS 콘텐츠 생성
|
||||
String generatedContent = aiContentGenerator.generateSnsContent(request);
|
||||
|
||||
// 플랫폼에 맞는 해시태그 생성
|
||||
Platform platform = Platform.fromString(request.getPlatform());
|
||||
List<String> hashtags = aiContentGenerator.generateHashtags(generatedContent, platform);
|
||||
|
||||
// 생성 조건 정보 구성
|
||||
CreationConditions conditions = CreationConditions.builder()
|
||||
.category(request.getCategory())
|
||||
.requirement(request.getRequirement())
|
||||
.toneAndManner(request.getToneAndManner())
|
||||
.emotionIntensity(request.getEmotionIntensity())
|
||||
.eventName(request.getEventName())
|
||||
.startDate(request.getStartDate())
|
||||
.endDate(request.getEndDate())
|
||||
.build();
|
||||
|
||||
// 임시 콘텐츠 생성 (저장하지 않음)
|
||||
Content content = Content.builder()
|
||||
// .contentType(ContentType.SNS_POST)
|
||||
.platform(platform)
|
||||
.title(request.getTitle())
|
||||
.content(generatedContent)
|
||||
.hashtags(hashtags)
|
||||
.images(request.getImages())
|
||||
.status(ContentStatus.DRAFT)
|
||||
.creationConditions(conditions)
|
||||
.storeId(request.getStoreId())
|
||||
.createdAt(LocalDateTime.now())
|
||||
.updatedAt(LocalDateTime.now())
|
||||
.build();
|
||||
String content = aiContentGenerator.generateSnsContent(request);
|
||||
|
||||
return SnsContentCreateResponse.builder()
|
||||
.contentId(null) // 임시 생성이므로 ID 없음
|
||||
.contentType(content.getContentType().name())
|
||||
.platform(content.getPlatform().name())
|
||||
.title(content.getTitle())
|
||||
.content(content.getContent())
|
||||
.hashtags(content.getHashtags())
|
||||
.fixedImages(content.getImages())
|
||||
.status(content.getStatus().name())
|
||||
.createdAt(content.getCreatedAt())
|
||||
.content(content)
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -98,8 +61,8 @@ public class SnsContentService implements SnsContentUseCase {
|
||||
CreationConditions conditions = CreationConditions.builder()
|
||||
.category(request.getCategory())
|
||||
.requirement(request.getRequirement())
|
||||
.toneAndManner(request.getToneAndManner())
|
||||
.emotionIntensity(request.getEmotionIntensity())
|
||||
//.toneAndManner(request.getToneAndManner())
|
||||
//.emotionIntensity(request.getEmotionIntensity())
|
||||
.eventName(request.getEventName())
|
||||
.startDate(request.getStartDate())
|
||||
.endDate(request.getEndDate())
|
||||
|
||||
@ -44,7 +44,7 @@ public interface ContentQueryUseCase {
|
||||
* @param contentId 콘텐츠 ID
|
||||
* @return 콘텐츠 상세 정보
|
||||
*/
|
||||
ContentDetailResponse getContentDetail(Long contentId);
|
||||
//ContentDetailResponse getContentDetail(Long contentId);
|
||||
|
||||
/**
|
||||
* 콘텐츠 삭제
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
// marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java
|
||||
package com.won.smarketing.content.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 org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* WebClient 설정
|
||||
* Python AI 서비스 호출을 위한 WebClient 구성
|
||||
*/
|
||||
@Configuration
|
||||
public class WebClientConfig {
|
||||
|
||||
@Bean
|
||||
public WebClient webClient() {
|
||||
HttpClient httpClient = HttpClient.create()
|
||||
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
|
||||
.responseTimeout(Duration.ofMillis(30000));
|
||||
|
||||
return WebClient.builder()
|
||||
.clientConnector(new ReactorClientHttpConnector(httpClient))
|
||||
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@ -24,8 +24,11 @@ public class CreationConditions {
|
||||
private String id;
|
||||
private String category;
|
||||
private String requirement;
|
||||
private String toneAndManner;
|
||||
private String emotionIntensity;
|
||||
// private String toneAndManner;
|
||||
// private String emotionIntensity;
|
||||
private String storeName;
|
||||
private String storeType;
|
||||
private String target;
|
||||
private String eventName;
|
||||
private LocalDate startDate;
|
||||
private LocalDate endDate;
|
||||
|
||||
@ -2,6 +2,7 @@ package com.won.smarketing.content.domain.service;
|
||||
|
||||
import com.won.smarketing.content.domain.model.Platform;
|
||||
import com.won.smarketing.content.presentation.dto.SnsContentCreateRequest;
|
||||
import com.won.smarketing.content.presentation.dto.SnsContentCreateResponse;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -18,13 +19,4 @@ public interface AiContentGenerator {
|
||||
* @return 생성된 콘텐츠
|
||||
*/
|
||||
String generateSnsContent(SnsContentCreateRequest request);
|
||||
|
||||
/**
|
||||
* 플랫폼별 해시태그 생성
|
||||
*
|
||||
* @param content 콘텐츠 내용
|
||||
* @param platform 플랫폼
|
||||
* @return 해시태그 목록
|
||||
*/
|
||||
List<String> generateHashtags(String content, Platform platform);
|
||||
}
|
||||
|
||||
@ -1,95 +1,99 @@
|
||||
// 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.presentation.dto.SnsContentCreateRequest;
|
||||
import com.won.smarketing.content.presentation.dto.SnsContentCreateResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Claude AI를 활용한 콘텐츠 생성 구현체
|
||||
* Clean Architecture의 Infrastructure Layer에 위치
|
||||
* Python AI SNS Content Service를 활용한 콘텐츠 생성 구현체
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ClaudeAiContentGenerator implements AiContentGenerator {
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
@Value("${external.ai-service.base-url:http://20.249.139.88:5001}")
|
||||
private String aiServiceBaseUrl;
|
||||
|
||||
/**
|
||||
* SNS 콘텐츠 생성
|
||||
* SNS 콘텐츠 생성 - Python AI 서비스 호출
|
||||
*/
|
||||
@Override
|
||||
public String generateSnsContent(SnsContentCreateRequest request) {
|
||||
try {
|
||||
String prompt = buildContentPrompt(request);
|
||||
return generateDummySnsContent(request.getTitle(), Platform.fromString(request.getPlatform()));
|
||||
} catch (Exception e) {
|
||||
log.error("AI 콘텐츠 생성 실패: {}", e.getMessage(), e);
|
||||
return generateFallbackContent(request.getTitle(), Platform.fromString(request.getPlatform()));
|
||||
log.info("Python AI 서비스 호출: {}/api/ai/sns", aiServiceBaseUrl);
|
||||
|
||||
// 요청 데이터 구성
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("storeId", request.getStoreId());
|
||||
requestBody.put("storeName", request.getStoreName());
|
||||
requestBody.put("storeType", request.getStoreType());
|
||||
requestBody.put("platform", request.getPlatform());
|
||||
requestBody.put("title", request.getTitle());
|
||||
requestBody.put("category", request.getCategory());
|
||||
requestBody.put("contentType", request.getContentType());
|
||||
requestBody.put("requirement", request.getRequirement());
|
||||
|
||||
//requestBody.put("tone_and_manner", request.getToneAndManner());
|
||||
// requestBody.put("emotion_intensity", request.getEmotionIntensity());
|
||||
requestBody.put("target", request.getTarget());
|
||||
|
||||
requestBody.put("event_name", request.getEventName());
|
||||
requestBody.put("start_date", request.getStartDate());
|
||||
requestBody.put("end_date", request.getEndDate());
|
||||
|
||||
requestBody.put("images", request.getImages());
|
||||
|
||||
// Python AI 서비스 호출
|
||||
Map<String, Object> response = webClient
|
||||
.method(HttpMethod.GET)
|
||||
.uri(aiServiceBaseUrl + "/api/ai/sns")
|
||||
.header("Content-Type", "application/json")
|
||||
.bodyValue(requestBody)
|
||||
.retrieve()
|
||||
.bodyToMono(Map.class)
|
||||
.timeout(Duration.ofSeconds(30))
|
||||
.block();
|
||||
|
||||
String content = "";
|
||||
|
||||
// 응답에서 content 추출
|
||||
if (response != null && response.containsKey("content")) {
|
||||
content = (String) response.get("content");
|
||||
log.info("AI 서비스 응답 성공: contentLength={}", content.length());
|
||||
|
||||
return content;
|
||||
}
|
||||
return content;
|
||||
// } catch (Exception e) {
|
||||
// log.error("AI 서비스 호출 실패: {}", e.getMessage(), e);
|
||||
// return generateFallbackContent(request.getTitle(), Platform.fromString(request.getPlatform()));
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* 플랫폼별 해시태그 생성
|
||||
* 폴백 콘텐츠 생성
|
||||
*/
|
||||
@Override
|
||||
public List<String> generateHashtags(String content, Platform platform) {
|
||||
try {
|
||||
return generateDummyHashtags(platform);
|
||||
} catch (Exception e) {
|
||||
log.error("해시태그 생성 실패: {}", e.getMessage(), e);
|
||||
return generateFallbackHashtags();
|
||||
}
|
||||
}
|
||||
|
||||
private String buildContentPrompt(SnsContentCreateRequest request) {
|
||||
StringBuilder prompt = new StringBuilder();
|
||||
prompt.append("제목: ").append(request.getTitle()).append("\n");
|
||||
prompt.append("카테고리: ").append(request.getCategory()).append("\n");
|
||||
prompt.append("플랫폼: ").append(request.getPlatform()).append("\n");
|
||||
|
||||
if (request.getRequirement() != null) {
|
||||
prompt.append("요구사항: ").append(request.getRequirement()).append("\n");
|
||||
}
|
||||
|
||||
if (request.getToneAndManner() != null) {
|
||||
prompt.append("톤앤매너: ").append(request.getToneAndManner()).append("\n");
|
||||
}
|
||||
|
||||
return prompt.toString();
|
||||
}
|
||||
|
||||
private String generateDummySnsContent(String title, Platform platform) {
|
||||
String baseContent = "🌟 " + title + "를 소개합니다! 🌟\n\n" +
|
||||
"저희 매장에서 특별한 경험을 만나보세요.\n" +
|
||||
"고객 여러분의 소중한 시간을 더욱 특별하게 만들어드리겠습니다.\n\n";
|
||||
|
||||
if (platform == Platform.INSTAGRAM) {
|
||||
return baseContent + "더 많은 정보는 프로필 링크에서 확인하세요! 📸";
|
||||
} else {
|
||||
return baseContent + "자세한 내용은 저희 블로그를 방문해 주세요! ✨";
|
||||
}
|
||||
}
|
||||
|
||||
private String generateFallbackContent(String title, Platform platform) {
|
||||
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("#소상공인", "#마케팅", "#홍보");
|
||||
}
|
||||
}
|
||||
// private String generateFallbackContent(String title, Platform platform) {
|
||||
// String baseContent = "🌟 " + title + "를 소개합니다! 🌟\n\n" +
|
||||
// "저희 매장에서 특별한 경험을 만나보세요.\n" +
|
||||
// "고객 여러분의 소중한 시간을 더욱 특별하게 만들어드리겠습니다.\n\n";
|
||||
//
|
||||
// if (platform == Platform.INSTAGRAM) {
|
||||
// return baseContent + "더 많은 정보는 프로필 링크에서 확인하세요! 📸";
|
||||
// } else {
|
||||
// return baseContent + "자세한 내용은 저희 블로그를 방문해 주세요! ✨";
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@ -105,8 +105,8 @@ public class ContentMapper {
|
||||
ContentConditionsJpaEntity entity = new ContentConditionsJpaEntity();
|
||||
entity.setCategory(conditions.getCategory());
|
||||
entity.setRequirement(conditions.getRequirement());
|
||||
entity.setToneAndManner(conditions.getToneAndManner());
|
||||
entity.setEmotionIntensity(conditions.getEmotionIntensity());
|
||||
// entity.setToneAndManner(conditions.getToneAndManner());
|
||||
// entity.setEmotionIntensity(conditions.getEmotionIntensity());
|
||||
entity.setEventName(conditions.getEventName());
|
||||
entity.setStartDate(conditions.getStartDate());
|
||||
entity.setEndDate(conditions.getEndDate());
|
||||
@ -126,8 +126,8 @@ public class ContentMapper {
|
||||
return CreationConditions.builder()
|
||||
.category(entity.getCategory())
|
||||
.requirement(entity.getRequirement())
|
||||
.toneAndManner(entity.getToneAndManner())
|
||||
.emotionIntensity(entity.getEmotionIntensity())
|
||||
// .toneAndManner(entity.getToneAndManner())
|
||||
// .emotionIntensity(entity.getEmotionIntensity())
|
||||
.eventName(entity.getEventName())
|
||||
.startDate(entity.getStartDate())
|
||||
.endDate(entity.getEndDate())
|
||||
|
||||
@ -143,14 +143,14 @@ public class ContentController {
|
||||
* @param contentId 조회할 콘텐츠 ID
|
||||
* @return 콘텐츠 상세 정보
|
||||
*/
|
||||
@Operation(summary = "콘텐츠 상세 조회", description = "특정 콘텐츠의 상세 정보를 조회합니다.")
|
||||
@GetMapping("/{contentId}")
|
||||
public ResponseEntity<ApiResponse<ContentDetailResponse>> getContentDetail(
|
||||
@Parameter(description = "콘텐츠 ID", required = true)
|
||||
@PathVariable Long contentId) {
|
||||
ContentDetailResponse response = contentQueryUseCase.getContentDetail(contentId);
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
// @Operation(summary = "콘텐츠 상세 조회", description = "특정 콘텐츠의 상세 정보를 조회합니다.")
|
||||
// @GetMapping("/{contentId}")
|
||||
// public ResponseEntity<ApiResponse<ContentDetailResponse>> getContentDetail(
|
||||
// @Parameter(description = "콘텐츠 ID", required = true)
|
||||
// @PathVariable Long contentId) {
|
||||
// ContentDetailResponse response = contentQueryUseCase.getContentDetail(contentId);
|
||||
// return ResponseEntity.ok(ApiResponse.success(response));
|
||||
// }
|
||||
|
||||
/**
|
||||
* 콘텐츠 삭제
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.won.smarketing.content.presentation.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
@ -23,6 +24,7 @@ import java.util.List;
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "SNS 콘텐츠 생성 요청")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class SnsContentCreateRequest {
|
||||
|
||||
// ==================== 기본 정보 ====================
|
||||
@ -31,6 +33,12 @@ public class SnsContentCreateRequest {
|
||||
@NotNull(message = "매장 ID는 필수입니다")
|
||||
private Long storeId;
|
||||
|
||||
@Schema(description = "매장 이름", example = "명륜진사갈비")
|
||||
private String storeName;
|
||||
|
||||
@Schema(description = "업종", example = "한식")
|
||||
private String storeType;
|
||||
|
||||
@Schema(description = "대상 플랫폼",
|
||||
example = "INSTAGRAM",
|
||||
allowableValues = {"INSTAGRAM", "NAVER_BLOG", "FACEBOOK", "KAKAO_STORY"},
|
||||
@ -54,15 +62,21 @@ public class SnsContentCreateRequest {
|
||||
@Size(max = 500, message = "요구사항은 500자 이하로 입력해주세요")
|
||||
private String requirement;
|
||||
|
||||
@Schema(description = "톤앤매너",
|
||||
example = "친근함",
|
||||
allowableValues = {"친근함", "전문적", "유머러스", "감성적", "트렌디"})
|
||||
private String toneAndManner;
|
||||
@Schema(description = "타겟층", example = "10대 청소년")
|
||||
private String target;
|
||||
|
||||
@Schema(description = "감정 강도",
|
||||
example = "보통",
|
||||
allowableValues = {"약함", "보통", "강함"})
|
||||
private String emotionIntensity;
|
||||
@Schema(description = "콘텐츠 타입", example = "SNS 게시물")
|
||||
private String contentType;
|
||||
|
||||
// @Schema(description = "톤앤매너",
|
||||
// example = "친근함",
|
||||
// allowableValues = {"친근함", "전문적", "유머러스", "감성적", "트렌디"})
|
||||
// private String toneAndManner;
|
||||
|
||||
// @Schema(description = "감정 강도",
|
||||
// example = "보통",
|
||||
// allowableValues = {"약함", "보통", "강함"})
|
||||
// private String emotionIntensity;
|
||||
|
||||
// ==================== 이벤트 정보 ====================
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.won.smarketing.content.presentation.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
@ -20,364 +21,8 @@ import java.util.List;
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "SNS 콘텐츠 생성 응답")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class SnsContentCreateResponse {
|
||||
|
||||
// ==================== 기본 식별 정보 ====================
|
||||
|
||||
@Schema(description = "생성된 콘텐츠 ID", example = "1")
|
||||
private Long contentId;
|
||||
|
||||
@Schema(description = "콘텐츠 타입", example = "SNS_POST")
|
||||
private String contentType;
|
||||
|
||||
@Schema(description = "대상 플랫폼", example = "INSTAGRAM",
|
||||
allowableValues = {"INSTAGRAM", "NAVER_BLOG", "FACEBOOK", "KAKAO_STORY"})
|
||||
private String platform;
|
||||
|
||||
// ==================== AI 생성 콘텐츠 ====================
|
||||
|
||||
@Schema(description = "AI가 생성한 콘텐츠 제목",
|
||||
example = "맛있는 신메뉴를 소개합니다! ✨")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "AI가 생성한 콘텐츠 내용",
|
||||
example = "안녕하세요! 😊\n\n특별한 신메뉴가 출시되었습니다!\n진짜 맛있어서 꼭 한번 드셔보세요 🍽️\n\n매장에서 기다리고 있을게요! 💫")
|
||||
@Schema(description = "생성된 콘텐츠")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "AI가 생성한 해시태그 목록",
|
||||
example = "[\"맛집\", \"신메뉴\", \"추천\", \"인스타그램\", \"일상\", \"좋아요\", \"팔로우\", \"맛있어요\"]")
|
||||
private List<String> hashtags;
|
||||
|
||||
// ==================== 플랫폼별 최적화 정보 ====================
|
||||
|
||||
@Schema(description = "플랫폼별 최적화된 콘텐츠 길이", example = "280")
|
||||
private Integer contentLength;
|
||||
|
||||
@Schema(description = "플랫폼별 권장 해시태그 개수", example = "8")
|
||||
private Integer recommendedHashtagCount;
|
||||
|
||||
@Schema(description = "플랫폼별 최대 해시태그 개수", example = "15")
|
||||
private Integer maxHashtagCount;
|
||||
|
||||
// ==================== 생성 조건 정보 ====================
|
||||
|
||||
@Schema(description = "콘텐츠 생성에 사용된 조건들")
|
||||
private GenerationConditionsDto generationConditions;
|
||||
|
||||
// ==================== 상태 및 메타데이터 ====================
|
||||
|
||||
@Schema(description = "생성 상태", example = "DRAFT",
|
||||
allowableValues = {"DRAFT", "PUBLISHED", "SCHEDULED"})
|
||||
private String status;
|
||||
|
||||
@Schema(description = "생성 일시", example = "2024-01-15T10:30:00")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Schema(description = "AI 모델 버전", example = "gpt-4-turbo")
|
||||
private String aiModelVersion;
|
||||
|
||||
@Schema(description = "생성 시간 (초)", example = "3.5")
|
||||
private Double generationTimeSeconds;
|
||||
|
||||
// ==================== 추가 정보 ====================
|
||||
|
||||
@Schema(description = "업로드된 원본 이미지 URL 목록")
|
||||
private List<String> originalImages;
|
||||
|
||||
@Schema(description = "콘텐츠 품질 점수 (1-100)", example = "85")
|
||||
private Integer qualityScore;
|
||||
|
||||
@Schema(description = "예상 참여율 (%)", example = "12.5")
|
||||
private Double expectedEngagementRate;
|
||||
|
||||
@Schema(description = "콘텐츠 카테고리", example = "음식/메뉴소개")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "보정된 이미지 URL 목록")
|
||||
private List<String> fixedImages;
|
||||
|
||||
// ==================== 편집 가능 여부 ====================
|
||||
|
||||
@Schema(description = "제목 편집 가능 여부", example = "true")
|
||||
@Builder.Default
|
||||
private Boolean titleEditable = true;
|
||||
|
||||
@Schema(description = "내용 편집 가능 여부", example = "true")
|
||||
@Builder.Default
|
||||
private Boolean contentEditable = true;
|
||||
|
||||
@Schema(description = "해시태그 편집 가능 여부", example = "true")
|
||||
@Builder.Default
|
||||
private Boolean hashtagsEditable = true;
|
||||
|
||||
// ==================== 대안 콘텐츠 ====================
|
||||
|
||||
@Schema(description = "대안 제목 목록 (사용자 선택용)")
|
||||
private List<String> alternativeTitles;
|
||||
|
||||
@Schema(description = "대안 해시태그 세트 목록")
|
||||
private List<List<String>> alternativeHashtagSets;
|
||||
|
||||
// ==================== 내부 DTO 클래스 ====================
|
||||
|
||||
/**
|
||||
* 콘텐츠 생성 조건 DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "콘텐츠 생성 조건")
|
||||
public static class GenerationConditionsDto {
|
||||
|
||||
@Schema(description = "홍보 대상", example = "메뉴")
|
||||
private String targetAudience;
|
||||
|
||||
@Schema(description = "이벤트명", example = "신메뉴 출시 이벤트")
|
||||
private String eventName;
|
||||
|
||||
@Schema(description = "톤앤매너", example = "친근함")
|
||||
private String toneAndManner;
|
||||
|
||||
@Schema(description = "프로모션 유형", example = "할인 정보")
|
||||
private String promotionType;
|
||||
|
||||
@Schema(description = "감정 강도", example = "보통")
|
||||
private String emotionIntensity;
|
||||
|
||||
@Schema(description = "홍보 시작일", example = "2024-01-15T09:00:00")
|
||||
private LocalDateTime promotionStartDate;
|
||||
|
||||
@Schema(description = "홍보 종료일", example = "2024-01-22T23:59:59")
|
||||
private LocalDateTime promotionEndDate;
|
||||
}
|
||||
|
||||
// ==================== 비즈니스 메서드 ====================
|
||||
|
||||
/**
|
||||
* 플랫폼별 콘텐츠 최적화 여부 확인
|
||||
*
|
||||
* @return 콘텐츠가 플랫폼 권장 사항을 만족하면 true
|
||||
*/
|
||||
public boolean isOptimizedForPlatform() {
|
||||
if (content == null || hashtags == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 플랫폼별 최적화 기준
|
||||
switch (platform.toUpperCase()) {
|
||||
case "INSTAGRAM":
|
||||
return content.length() <= 2200 &&
|
||||
hashtags.size() <= 15 &&
|
||||
hashtags.size() >= 5;
|
||||
case "NAVER_BLOG":
|
||||
return content.length() >= 300 &&
|
||||
hashtags.size() <= 10 &&
|
||||
hashtags.size() >= 3;
|
||||
case "FACEBOOK":
|
||||
return content.length() <= 500 &&
|
||||
hashtags.size() <= 5;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 고품질 콘텐츠 여부 확인
|
||||
*
|
||||
* @return 품질 점수가 80점 이상이면 true
|
||||
*/
|
||||
public boolean isHighQuality() {
|
||||
return qualityScore != null && qualityScore >= 80;
|
||||
}
|
||||
|
||||
/**
|
||||
* 참여율 예상 등급 반환
|
||||
*
|
||||
* @return 예상 참여율 등급 (HIGH, MEDIUM, LOW)
|
||||
*/
|
||||
public String getExpectedEngagementLevel() {
|
||||
if (expectedEngagementRate == null) {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
if (expectedEngagementRate >= 15.0) {
|
||||
return "HIGH";
|
||||
} else if (expectedEngagementRate >= 8.0) {
|
||||
return "MEDIUM";
|
||||
} else {
|
||||
return "LOW";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 해시태그를 문자열로 변환 (# 포함)
|
||||
*
|
||||
* @return #으로 시작하는 해시태그 문자열
|
||||
*/
|
||||
public String getHashtagsAsString() {
|
||||
if (hashtags == null || hashtags.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return hashtags.stream()
|
||||
.map(tag -> "#" + tag)
|
||||
.reduce((a, b) -> a + " " + b)
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 콘텐츠 요약 생성
|
||||
*
|
||||
* @param maxLength 최대 길이
|
||||
* @return 요약된 콘텐츠
|
||||
*/
|
||||
public String getContentSummary(int maxLength) {
|
||||
if (content == null || content.length() <= maxLength) {
|
||||
return content;
|
||||
}
|
||||
return content.substring(0, maxLength) + "...";
|
||||
}
|
||||
|
||||
/**
|
||||
* 플랫폼별 최적화 제안사항 반환
|
||||
*
|
||||
* @return 최적화 제안사항 목록
|
||||
*/
|
||||
public List<String> getOptimizationSuggestions() {
|
||||
List<String> suggestions = new java.util.ArrayList<>();
|
||||
|
||||
if (!isOptimizedForPlatform()) {
|
||||
switch (platform.toUpperCase()) {
|
||||
case "INSTAGRAM":
|
||||
if (content != null && content.length() > 2200) {
|
||||
suggestions.add("콘텐츠 길이를 2200자 이하로 줄여주세요.");
|
||||
}
|
||||
if (hashtags != null && hashtags.size() > 15) {
|
||||
suggestions.add("해시태그를 15개 이하로 줄여주세요.");
|
||||
}
|
||||
if (hashtags != null && hashtags.size() < 5) {
|
||||
suggestions.add("해시태그를 5개 이상 추가해주세요.");
|
||||
}
|
||||
break;
|
||||
case "NAVER_BLOG":
|
||||
if (content != null && content.length() < 300) {
|
||||
suggestions.add("블로그 포스팅을 위해 내용을 300자 이상으로 늘려주세요.");
|
||||
}
|
||||
if (hashtags != null && hashtags.size() > 10) {
|
||||
suggestions.add("네이버 블로그는 해시태그를 10개 이하로 사용하는 것이 좋습니다.");
|
||||
}
|
||||
break;
|
||||
case "FACEBOOK":
|
||||
if (content != null && content.length() > 500) {
|
||||
suggestions.add("페이스북에서는 500자 이하의 짧은 글이 더 효과적입니다.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
// ==================== 팩토리 메서드 ====================
|
||||
|
||||
/**
|
||||
* 도메인 엔티티에서 SnsContentCreateResponse 생성
|
||||
*
|
||||
* @param content 콘텐츠 도메인 엔티티
|
||||
* @param aiMetadata AI 생성 메타데이터
|
||||
* @return SnsContentCreateResponse
|
||||
*/
|
||||
public static SnsContentCreateResponse fromDomain(
|
||||
com.won.smarketing.content.domain.model.Content content,
|
||||
AiGenerationMetadata aiMetadata) {
|
||||
|
||||
SnsContentCreateResponseBuilder builder = SnsContentCreateResponse.builder()
|
||||
.contentId(content.getId())
|
||||
.contentType(content.getContentType().name())
|
||||
.platform(content.getPlatform().name())
|
||||
.title(content.getTitle())
|
||||
.content(content.getContent())
|
||||
.hashtags(content.getHashtags())
|
||||
.status(content.getStatus().name())
|
||||
.createdAt(content.getCreatedAt())
|
||||
.originalImages(content.getImages());
|
||||
|
||||
// 생성 조건 정보 설정
|
||||
if (content.getCreationConditions() != null) {
|
||||
builder.generationConditions(GenerationConditionsDto.builder()
|
||||
//.targetAudience(content.getCreationConditions().getTargetAudience())
|
||||
.eventName(content.getCreationConditions().getEventName())
|
||||
.toneAndManner(content.getCreationConditions().getToneAndManner())
|
||||
.promotionType(content.getCreationConditions().getPromotionType())
|
||||
.emotionIntensity(content.getCreationConditions().getEmotionIntensity())
|
||||
.promotionStartDate(content.getPromotionStartDate())
|
||||
.promotionEndDate(content.getPromotionEndDate())
|
||||
.build());
|
||||
}
|
||||
|
||||
// AI 메타데이터 설정
|
||||
if (aiMetadata != null) {
|
||||
builder.aiModelVersion(aiMetadata.getModelVersion())
|
||||
.generationTimeSeconds(aiMetadata.getGenerationTime())
|
||||
.qualityScore(aiMetadata.getQualityScore())
|
||||
.expectedEngagementRate(aiMetadata.getExpectedEngagementRate())
|
||||
.alternativeTitles(aiMetadata.getAlternativeTitles())
|
||||
.alternativeHashtagSets(aiMetadata.getAlternativeHashtagSets());
|
||||
}
|
||||
|
||||
// 플랫폼별 최적화 정보 설정
|
||||
SnsContentCreateResponse response = builder.build();
|
||||
response.setContentLength(response.getContent() != null ? response.getContent().length() : 0);
|
||||
response.setRecommendedHashtagCount(getRecommendedHashtagCount(content.getPlatform().name()));
|
||||
response.setMaxHashtagCount(getMaxHashtagCount(content.getPlatform().name()));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 플랫폼별 권장 해시태그 개수 반환
|
||||
*/
|
||||
private static Integer getRecommendedHashtagCount(String platform) {
|
||||
switch (platform.toUpperCase()) {
|
||||
case "INSTAGRAM": return 8;
|
||||
case "NAVER_BLOG": return 5;
|
||||
case "FACEBOOK": return 3;
|
||||
case "KAKAO_STORY": return 5;
|
||||
default: return 5;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 플랫폼별 최대 해시태그 개수 반환
|
||||
*/
|
||||
private static Integer getMaxHashtagCount(String platform) {
|
||||
switch (platform.toUpperCase()) {
|
||||
case "INSTAGRAM": return 15;
|
||||
case "NAVER_BLOG": return 10;
|
||||
case "FACEBOOK": return 5;
|
||||
case "KAKAO_STORY": return 8;
|
||||
default: return 10;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== AI 생성 메타데이터 DTO ====================
|
||||
|
||||
/**
|
||||
* AI 생성 메타데이터
|
||||
* AI 생성 과정에서 나온 부가 정보들
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public static class AiGenerationMetadata {
|
||||
private String modelVersion;
|
||||
private Double generationTime;
|
||||
private Integer qualityScore;
|
||||
private Double expectedEngagementRate;
|
||||
private List<String> alternativeTitles;
|
||||
private List<List<String>> alternativeHashtagSets;
|
||||
private String category;
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,9 @@ server:
|
||||
port: ${SERVER_PORT:8083}
|
||||
|
||||
spring:
|
||||
jackson:
|
||||
deserialization:
|
||||
fail-on-unknown-properties: false
|
||||
application:
|
||||
name: marketing-content-service
|
||||
datasource:
|
||||
@ -31,3 +34,6 @@ jwt:
|
||||
logging:
|
||||
level:
|
||||
com.won.smarketing: ${LOG_LEVEL:DEBUG}
|
||||
external:
|
||||
ai-service:
|
||||
base-url: ${AI_SERVICE_BASE_URL:http://20.249.139.88:5001}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user