update marketing-content

update marketing-content
This commit is contained in:
박서은 2025-06-11 16:30:30 +09:00
parent b854885d2e
commit c5a7254ce5
16 changed files with 341 additions and 38 deletions

View File

@ -1,4 +1,4 @@
dependencies {
implementation project(':common')
runtimeOnly 'com.mysql:mysql-connector-j'
runtimeOnly 'org.postgresql:postgresql'
}

View File

@ -40,18 +40,18 @@ public class ContentQueryService implements ContentQueryUseCase {
// 제목과 기간 업데이트
content.updateTitle(request.getTitle());
content.updatePeriod(request.getStartDate(), request.getEndDate());
content.updatePeriod(request.getPromotionStartDate(), request.getPromotionEndDate());
Content updatedContent = contentRepository.save(content);
return ContentUpdateResponse.builder()
.contentId(updatedContent.getId().getValue())
.contentType(updatedContent.getContentType().name())
.platform(updatedContent.getPlatform().name())
.contentId(updatedContent.getId())
//.contentType(updatedContent.getContentType().name())
//.platform(updatedContent.getPlatform().name())
.title(updatedContent.getTitle())
.content(updatedContent.getContent())
.hashtags(updatedContent.getHashtags())
.images(updatedContent.getImages())
//.hashtags(updatedContent.getHashtags())
//.images(updatedContent.getImages())
.status(updatedContent.getStatus().name())
.updatedAt(updatedContent.getUpdatedAt())
.build();
@ -105,7 +105,7 @@ public class ContentQueryService implements ContentQueryUseCase {
.orElseThrow(() -> new BusinessException(ErrorCode.CONTENT_NOT_FOUND));
return ContentDetailResponse.builder()
.contentId(content.getId().getValue())
.contentId(content.getId())
.contentType(content.getContentType().name())
.platform(content.getPlatform().name())
.title(content.getTitle())
@ -140,7 +140,7 @@ public class ContentQueryService implements ContentQueryUseCase {
*/
private ContentResponse toContentResponse(Content content) {
return ContentResponse.builder()
.contentId(content.getId().getValue())
.contentId(content.getId())
.contentType(content.getContentType().name())
.platform(content.getPlatform().name())
.title(content.getTitle())
@ -161,13 +161,13 @@ public class ContentQueryService implements ContentQueryUseCase {
*/
private OngoingContentResponse toOngoingContentResponse(Content content) {
return OngoingContentResponse.builder()
.contentId(content.getId().getValue())
.contentId(content.getId())
.contentType(content.getContentType().name())
.platform(content.getPlatform().name())
.title(content.getTitle())
.status(content.getStatus().name())
.createdAt(content.getCreatedAt())
.viewCount(0) // TODO: 실제 조회 구현 필요
.promotionStartDate(content.getPromotionStartDate())
//.viewCount(0) // TODO: 실제 조회 구현 필요
.build();
}

View File

@ -61,10 +61,10 @@ public class PosterContentService implements PosterContentUseCase {
.contentId(null) // 임시 생성이므로 ID 없음
.contentType(ContentType.POSTER.name())
.title(request.getTitle())
.image(generatedPoster)
.posterImage(generatedPoster)
.posterSizes(posterSizes)
.status(ContentStatus.DRAFT.name())
.createdAt(LocalDateTime.now())
//.createdAt(LocalDateTime.now())
.build();
}

View File

@ -39,7 +39,7 @@ public class SnsContentService implements SnsContentUseCase {
*/
@Override
@Transactional
/* public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request) {
public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request) {
// AI를 사용하여 SNS 콘텐츠 생성
String generatedContent = aiContentGenerator.generateSnsContent(request);
@ -80,11 +80,11 @@ public class SnsContentService implements SnsContentUseCase {
.title(content.getTitle())
.content(content.getContent())
.hashtags(content.getHashtags())
.images(content.getImages())
.fixedImages(content.getImages())
.status(content.getStatus().name())
.createdAt(content.getCreatedAt())
.build();
}*/
}
/**
* SNS 콘텐츠 저장

View File

@ -216,6 +216,24 @@ public class Content {
this.promotionEndDate = endDate;
}
/**
* 홍보 기간 설정
*
* 비즈니스 규칙:
* - 시작일은 종료일보다 이전이어야
* - 과거 날짜로 설정 불가 (현재 시간 기준)
*
* @param startDate 홍보 시작일
* @param endDate 홍보 종료일
* @throws IllegalArgumentException 날짜가 유효하지 않은 경우
*/
public void updatePeriod(LocalDateTime startDate, LocalDateTime endDate) {
validatePromotionPeriod(startDate, endDate);
this.promotionStartDate = startDate;
this.promotionEndDate = endDate;
}
/**
* 해시태그 추가
*

View File

@ -53,4 +53,14 @@ public class CreationConditions {
* 사진 스타일 (포스터용)
*/
private String photoStyle;
/**
* 타겟 고객
*/
private String targetAudience;
/**
* 프로모션 타입
*/
private String promotionType;
}

View File

@ -12,7 +12,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import jakarta.validation.Valid;
import java.util.List;
/**

View File

@ -98,6 +98,9 @@ public class ContentResponse {
@Schema(description = "해시태그 개수", example = "8")
private Integer hashtagCount;
@Schema(description = "조회수", example = "8")
private Integer viewCount;
// ==================== 비즈니스 메서드 ====================
/**
@ -227,7 +230,7 @@ public class ContentResponse {
*/
public static ContentResponse fromDomain(com.won.smarketing.content.domain.model.Content content) {
ContentResponseBuilder builder = ContentResponse.builder()
.contentId(content.getId().getValue())
.contentId(content.getId())
.contentType(content.getContentType().name())
.platform(content.getPlatform().name())
.title(content.getTitle())

View File

@ -1,3 +1,4 @@
// smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentCreateRequest.java
package com.won.smarketing.content.presentation.dto;
import io.swagger.v3.oas.annotations.media.Schema;
@ -8,6 +9,7 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@ -20,6 +22,13 @@ import java.util.List;
@Schema(description = "포스터 콘텐츠 생성 요청")
public class PosterContentCreateRequest {
@Schema(description = "매장 ID", example = "1", required = true)
@NotNull(message = "매장 ID는 필수입니다")
private Long storeId;
@Schema(description = "제목", example = "특별 이벤트 안내")
private String title;
@Schema(description = "홍보 대상", example = "메뉴", required = true)
@NotBlank(message = "홍보 대상은 필수입니다")
private String targetAudience;
@ -48,4 +57,23 @@ public class PosterContentCreateRequest {
@NotNull(message = "이미지는 1개 이상 필수입니다")
@Size(min = 1, message = "이미지는 1개 이상 업로드해야 합니다")
private List<String> images;
// CreationConditions에 필요한 필드들
@Schema(description = "콘텐츠 카테고리", example = "이벤트")
private String category;
@Schema(description = "구체적인 요구사항", example = "신메뉴 출시 이벤트 포스터를 만들어주세요")
private String requirement;
@Schema(description = "톤앤매너", example = "전문적")
private String toneAndManner;
@Schema(description = "이벤트 시작일", example = "2024-01-15")
private LocalDate startDate;
@Schema(description = "이벤트 종료일", example = "2024-01-31")
private LocalDate endDate;
@Schema(description = "사진 스타일", example = "밝고 화사한")
private String photoStyle;
}

View File

@ -7,6 +7,7 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* 포스터 콘텐츠 생성 응답 DTO
@ -27,8 +28,11 @@ public class PosterContentCreateResponse {
@Schema(description = "생성된 포스터 텍스트 내용")
private String content;
@Schema(description = "포스터 이미지 URL 목록")
private List<String> posterImages;
@Schema(description = "생성된 포스터 타입")
private String contentType;
@Schema(description = "포스터 이미지 URL")
private String posterImage;
@Schema(description = "원본 이미지 URL 목록")
private List<String> originalImages;
@ -38,4 +42,8 @@ public class PosterContentCreateResponse {
@Schema(description = "생성 상태", example = "DRAFT")
private String status;
@Schema(description = "포스터사이즈", example = "800x600")
private Map<String, String> posterSizes;
}

View File

@ -1,3 +1,4 @@
// smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentSaveRequest.java
package com.won.smarketing.content.presentation.dto;
import io.swagger.v3.oas.annotations.media.Schema;
@ -6,6 +7,9 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.util.List;
/**
* 포스터 콘텐츠 저장 요청 DTO
*/
@ -19,15 +23,44 @@ public class PosterContentSaveRequest {
@NotNull(message = "콘텐츠 ID는 필수입니다")
private Long contentId;
@Schema(description = "최종 제목", example = "특별 이벤트 안내")
private String finalTitle;
@Schema(description = "매장 ID", example = "1", required = true)
@NotNull(message = "매장 ID는 필수입니다")
private Long storeId;
@Schema(description = "최종 콘텐츠 내용")
private String finalContent;
@Schema(description = "제목", example = "특별 이벤트 안내")
private String title;
@Schema(description = "콘텐츠 내용")
private String content;
@Schema(description = "선택된 포스터 이미지 URL")
private String selectedPosterImage;
private List<String> images;
@Schema(description = "발행 상태", example = "PUBLISHED")
private String status;
// CreationConditions에 필요한 필드들
@Schema(description = "콘텐츠 카테고리", example = "이벤트")
private String category;
@Schema(description = "구체적인 요구사항", example = "신메뉴 출시 이벤트 포스터를 만들어주세요")
private String requirement;
@Schema(description = "톤앤매너", example = "전문적")
private String toneAndManner;
@Schema(description = "감정 강도", example = "보통")
private String emotionIntensity;
@Schema(description = "이벤트명", example = "신메뉴 출시 이벤트")
private String eventName;
@Schema(description = "이벤트 시작일", example = "2024-01-15")
private LocalDate startDate;
@Schema(description = "이벤트 종료일", example = "2024-01-31")
private LocalDate endDate;
@Schema(description = "사진 스타일", example = "밝고 화사한")
private String photoStyle;
}

View File

@ -0,0 +1,160 @@
package com.won.smarketing.content.presentation.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.util.List;
/**
* SNS 콘텐츠 생성 요청 DTO
*
* AI 기반 SNS 콘텐츠 생성을 위한 요청 정보를 담고 있습니다.
* 사용자가 입력한 생성 조건을 바탕으로 AI가 적절한 SNS 콘텐츠를 생성합니다.
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "SNS 콘텐츠 생성 요청")
public class SnsContentCreateRequest {
// ==================== 기본 정보 ====================
@Schema(description = "매장 ID", example = "1", required = true)
@NotNull(message = "매장 ID는 필수입니다")
private Long storeId;
@Schema(description = "대상 플랫폼",
example = "INSTAGRAM",
allowableValues = {"INSTAGRAM", "NAVER_BLOG", "FACEBOOK", "KAKAO_STORY"},
required = true)
@NotBlank(message = "플랫폼은 필수입니다")
private String platform;
@Schema(description = "콘텐츠 제목", example = "1", required = true)
@NotNull(message = "콘텐츠 제목은 필수입니다")
private String title;
// ==================== 콘텐츠 생성 조건 ====================
@Schema(description = "콘텐츠 카테고리",
example = "메뉴소개",
allowableValues = {"메뉴소개", "이벤트", "일상", "인테리어", "고객후기", "기타"})
private String category;
@Schema(description = "구체적인 요구사항 또는 홍보하고 싶은 내용",
example = "새로 출시된 시그니처 버거를 홍보하고 싶어요")
@Size(max = 500, message = "요구사항은 500자 이하로 입력해주세요")
private String requirement;
@Schema(description = "톤앤매너",
example = "친근함",
allowableValues = {"친근함", "전문적", "유머러스", "감성적", "트렌디"})
private String toneAndManner;
@Schema(description = "감정 강도",
example = "보통",
allowableValues = {"약함", "보통", "강함"})
private String emotionIntensity;
// ==================== 이벤트 정보 ====================
@Schema(description = "이벤트명 (이벤트 콘텐츠인 경우)",
example = "신메뉴 출시 이벤트")
@Size(max = 200, message = "이벤트명은 200자 이하로 입력해주세요")
private String eventName;
@Schema(description = "이벤트 시작일 (이벤트 콘텐츠인 경우)",
example = "2024-01-15")
private LocalDate startDate;
@Schema(description = "이벤트 종료일 (이벤트 콘텐츠인 경우)",
example = "2024-01-31")
private LocalDate endDate;
// ==================== 미디어 정보 ====================
@Schema(description = "업로드된 이미지 파일 경로 목록")
private List<String> images;
@Schema(description = "사진 스타일 선호도",
example = "밝고 화사한",
allowableValues = {"밝고 화사한", "차분하고 세련된", "빈티지한", "모던한", "자연스러운"})
private String photoStyle;
// ==================== 추가 옵션 ====================
@Schema(description = "해시태그 포함 여부", example = "true")
@Builder.Default
private Boolean includeHashtags = true;
@Schema(description = "이모지 포함 여부", example = "true")
@Builder.Default
private Boolean includeEmojis = true;
@Schema(description = "콜투액션 포함 여부 (좋아요, 팔로우 요청 등)", example = "true")
@Builder.Default
private Boolean includeCallToAction = true;
@Schema(description = "매장 위치 정보 포함 여부", example = "false")
@Builder.Default
private Boolean includeLocation = false;
// ==================== 플랫폼별 옵션 ====================
@Schema(description = "인스타그램 스토리용 여부 (Instagram인 경우)", example = "false")
@Builder.Default
private Boolean forInstagramStory = false;
@Schema(description = "네이버 블로그 포스팅용 여부 (Naver Blog인 경우)", example = "false")
@Builder.Default
private Boolean forNaverBlogPost = false;
// ==================== AI 생성 옵션 ====================
@Schema(description = "대안 제목 생성 개수", example = "3")
@Builder.Default
private Integer alternativeTitleCount = 3;
@Schema(description = "대안 해시태그 세트 생성 개수", example = "2")
@Builder.Default
private Integer alternativeHashtagSetCount = 2;
@Schema(description = "AI 모델 버전 지정 (없으면 기본값 사용)", example = "gpt-4-turbo")
private String preferredAiModel;
// ==================== 검증 메서드 ====================
/**
* 이벤트 날짜 유효성 검증
* 시작일이 종료일보다 이후인지 확인
*/
public boolean isValidEventDates() {
if (startDate != null && endDate != null) {
return !startDate.isAfter(endDate);
}
return true;
}
/**
* 플랫폼별 필수 조건 검증
*/
public boolean isValidForPlatform() {
if ("INSTAGRAM".equals(platform)) {
// 인스타그램은 이미지가 권장됨
return images != null && !images.isEmpty();
}
if ("NAVER_BLOG".equals(platform)) {
// 네이버 블로그는 상세한 내용이 필요
return requirement != null && requirement.length() >= 20;
}
return true;
}
}

View File

@ -93,6 +93,9 @@ public class SnsContentCreateResponse {
@Schema(description = "콘텐츠 카테고리", example = "음식/메뉴소개")
private String category;
@Schema(description = "보정된 이미지 URL 목록")
private List<String> fixedImages;
// ==================== 편집 가능 여부 ====================
@Schema(description = "제목 편집 가능 여부", example = "true")

View File

@ -1,3 +1,4 @@
// smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/SnsContentSaveRequest.java
package com.won.smarketing.content.presentation.dto;
import io.swagger.v3.oas.annotations.media.Schema;
@ -8,6 +9,7 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@ -24,6 +26,26 @@ public class SnsContentSaveRequest {
@NotNull(message = "콘텐츠 ID는 필수입니다")
private Long contentId;
@Schema(description = "매장 ID", example = "1", required = true)
@NotNull(message = "매장 ID는 필수입니다")
private Long storeId;
@Schema(description = "플랫폼", example = "INSTAGRAM", required = true)
@NotBlank(message = "플랫폼은 필수입니다")
private String platform;
@Schema(description = "제목", example = "맛있는 신메뉴를 소개합니다!")
private String title;
@Schema(description = "콘텐츠 내용")
private String content;
@Schema(description = "해시태그 목록")
private List<String> hashtags;
@Schema(description = "이미지 URL 목록")
private List<String> images;
@Schema(description = "최종 제목", example = "맛있는 신메뉴를 소개합니다!")
private String finalTitle;
@ -32,4 +54,26 @@ public class SnsContentSaveRequest {
@Schema(description = "발행 상태", example = "PUBLISHED")
private String status;
// CreationConditions에 필요한 필드들
@Schema(description = "콘텐츠 카테고리", example = "메뉴소개")
private String category;
@Schema(description = "구체적인 요구사항", example = "새로 출시된 시그니처 버거를 홍보하고 싶어요")
private String requirement;
@Schema(description = "톤앤매너", example = "친근함")
private String toneAndManner;
@Schema(description = "감정 강도", example = "보통")
private String emotionIntensity;
@Schema(description = "이벤트명", example = "신메뉴 출시 이벤트")
private String eventName;
@Schema(description = "이벤트 시작일", example = "2024-01-15")
private LocalDate startDate;
@Schema(description = "이벤트 종료일", example = "2024-01-31")
private LocalDate endDate;
}

View File

@ -1,15 +1,14 @@
server:
port: ${SERVER_PORT:8083}
servlet:
context-path: /
spring:
application:
name: marketing-content-service
datasource:
url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:contentdb}
url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:MarketingContentDB}
username: ${POSTGRES_USER:postgres}
password: ${POSTGRES_PASSWORD:postgres}
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: ${JPA_DDL_AUTO:update}
@ -29,14 +28,10 @@ external:
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}
springdoc:
swagger-ui:
path: /swagger-ui.html
operations-sorter: method
api-docs:
path: /api-docs
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}

View File

@ -1,4 +1,5 @@
dependencies {
implementation project(':common')
runtimeOnly 'com.mysql:mysql-connector-j'
//
runtimeOnly 'org.postgresql:postgresql'
}