diff --git a/smarketing-java/marketing-content/build.gradle b/smarketing-java/marketing-content/build.gradle index 188d7bd..715bc47 100644 --- a/smarketing-java/marketing-content/build.gradle +++ b/smarketing-java/marketing-content/build.gradle @@ -1,4 +1,7 @@ dependencies { implementation project(':common') runtimeOnly 'org.postgresql:postgresql' + + // WebClient를 위한 Spring WebFlux 의존성 + implementation 'org.springframework.boot:spring-boot-starter-webflux' } \ No newline at end of file diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/ContentQueryService.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/ContentQueryService.java index c196e58..c205bd1 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/ContentQueryService.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/ContentQueryService.java @@ -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(); +// } } diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/PosterContentService.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/PosterContentService.java index 4db4d8a..e89b5c5 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/PosterContentService.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/PosterContentService.java @@ -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()) 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 dd8e603..af2f388 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 @@ -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 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()) diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/ContentQueryUseCase.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/ContentQueryUseCase.java index 0712961..c63b7c4 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/ContentQueryUseCase.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/ContentQueryUseCase.java @@ -44,7 +44,7 @@ public interface ContentQueryUseCase { * @param contentId 콘텐츠 ID * @return 콘텐츠 상세 정보 */ - ContentDetailResponse getContentDetail(Long contentId); + //ContentDetailResponse getContentDetail(Long contentId); /** * 콘텐츠 삭제 diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java new file mode 100644 index 0000000..7f7cf08 --- /dev/null +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java @@ -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(); + } +} \ 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 d7a9543..a284c2c 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 @@ -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; diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/AiContentGenerator.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/AiContentGenerator.java index 677853a..ad8f02d 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/AiContentGenerator.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/AiContentGenerator.java @@ -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 generateHashtags(String content, Platform platform); } 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 9d72f1f..63a5cb8 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,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 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 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 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 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 +// private String generateFallbackContent(String title, Platform platform) { +// String baseContent = "🌟 " + title + "를 소개합니다! 🌟\n\n" + +// "저희 매장에서 특별한 경험을 만나보세요.\n" + +// "고객 여러분의 소중한 시간을 더욱 특별하게 만들어드리겠습니다.\n\n"; +// +// if (platform == Platform.INSTAGRAM) { +// return baseContent + "더 많은 정보는 프로필 링크에서 확인하세요! 📸"; +// } else { +// return baseContent + "자세한 내용은 저희 블로그를 방문해 주세요! ✨"; +// } +// } +} 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 44fdb68..84836c5 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 @@ -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()) diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java index 4feb6b7..e527d8a 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/controller/ContentController.java @@ -143,14 +143,14 @@ public class ContentController { * @param contentId 조회할 콘텐츠 ID * @return 콘텐츠 상세 정보 */ - @Operation(summary = "콘텐츠 상세 조회", description = "특정 콘텐츠의 상세 정보를 조회합니다.") - @GetMapping("/{contentId}") - public ResponseEntity> 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> getContentDetail( +// @Parameter(description = "콘텐츠 ID", required = true) +// @PathVariable Long contentId) { +// ContentDetailResponse response = contentQueryUseCase.getContentDetail(contentId); +// return ResponseEntity.ok(ApiResponse.success(response)); +// } /** * 콘텐츠 삭제 diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateRequest.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateRequest.java index 70235b5..f8bcdeb 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateRequest.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentCreateRequest.java @@ -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; // ==================== 이벤트 정보 ==================== 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 0acf9ec..541dc4c 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 @@ -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 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 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 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 alternativeTitles; - - @Schema(description = "대안 해시태그 세트 목록") - private List> 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 getOptimizationSuggestions() { - List 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 alternativeTitles; - private List> alternativeHashtagSets; - private String category; - } } \ 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 10dc73d..59b0b54 100644 --- a/smarketing-java/marketing-content/src/main/resources/application.yml +++ b/smarketing-java/marketing-content/src/main/resources/application.yml @@ -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}