feat: poster content

This commit is contained in:
yuhalog 2025-06-17 17:26:36 +09:00
parent 1c79ea0834
commit 1d548a5f5c
8 changed files with 60 additions and 70 deletions

View File

@ -8,16 +8,17 @@ import com.won.smarketing.content.domain.model.CreationConditions;
import com.won.smarketing.content.domain.model.Platform;
import com.won.smarketing.content.domain.repository.ContentRepository;
import com.won.smarketing.content.domain.service.AiPosterGenerator;
import com.won.smarketing.content.domain.service.BlobStorageService;
import com.won.smarketing.content.presentation.dto.PosterContentCreateRequest;
import com.won.smarketing.content.presentation.dto.PosterContentCreateResponse;
import com.won.smarketing.content.presentation.dto.PosterContentSaveRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
/**
* 포스터 콘텐츠 서비스 구현체
@ -30,6 +31,7 @@ public class PosterContentService implements PosterContentUseCase {
private final ContentRepository contentRepository;
private final AiPosterGenerator aiPosterGenerator;
private final BlobStorageService blobStorageService;
/**
* 포스터 콘텐츠 생성
@ -39,10 +41,18 @@ public class PosterContentService implements PosterContentUseCase {
*/
@Override
@Transactional
public PosterContentCreateResponse generatePosterContent(PosterContentCreateRequest request) {
public PosterContentCreateResponse generatePosterContent(List<MultipartFile> images, PosterContentCreateRequest request) {
// 1. 이미지 blob storage에 저장하고 request 저장
List<String> imageUrls = blobStorageService.uploadImage(images, "poster-content-original");
request.setImages(imageUrls);
// 2. AI 요청
String generatedPoster = aiPosterGenerator.generatePoster(request);
// 3. 저장
Content savedContent = savePosterContent(request, generatedPoster);
// 생성 조건 정보 구성
CreationConditions conditions = CreationConditions.builder()
.category(request.getCategory())
@ -68,9 +78,8 @@ public class PosterContentService implements PosterContentUseCase {
*
* @param request 포스터 콘텐츠 저장 요청
*/
@Override
@Transactional
public void savePosterContent(PosterContentSaveRequest request) {
public Content savePosterContent(PosterContentCreateRequest request, String generatedPoster) {
// 생성 조건 구성
CreationConditions conditions = CreationConditions.builder()
.category(request.getCategory())
@ -86,7 +95,7 @@ public class PosterContentService implements PosterContentUseCase {
.contentType(ContentType.POSTER)
.platform(Platform.GENERAL)
.title(request.getTitle())
.content(request.getContent())
.content(generatedPoster)
.images(request.getImages())
.status(ContentStatus.PUBLISHED)
.creationConditions(conditions)
@ -94,6 +103,8 @@ public class PosterContentService implements PosterContentUseCase {
.build();
// 저장
contentRepository.save(content);
Content savedContent = contentRepository.save(content);
return savedContent;
}
}

View File

@ -4,6 +4,9 @@ package com.won.smarketing.content.application.usecase;
import com.won.smarketing.content.presentation.dto.PosterContentCreateRequest;
import com.won.smarketing.content.presentation.dto.PosterContentCreateResponse;
import com.won.smarketing.content.presentation.dto.PosterContentSaveRequest;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* 포스터 콘텐츠 관련 UseCase 인터페이스
@ -12,15 +15,10 @@ import com.won.smarketing.content.presentation.dto.PosterContentSaveRequest;
public interface PosterContentUseCase {
/**
* 포스터 콘텐츠 생성
* 포스터 콘텐츠 생성 저장
* @param request 포스터 콘텐츠 생성 요청
* @return 포스터 콘텐츠 생성 응답
*/
PosterContentCreateResponse generatePosterContent(PosterContentCreateRequest request);
PosterContentCreateResponse generatePosterContent(List<MultipartFile> images, PosterContentCreateRequest request);
/**
* 포스터 콘텐츠 저장
* @param request 포스터 콘텐츠 저장 요청
*/
void savePosterContent(PosterContentSaveRequest request);
}

View File

@ -27,42 +27,37 @@ import java.util.List;
@Builder
public class Content {
// ==================== 기본키 식별자 ====================
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "content_id")
private Long id;
// ==================== 콘텐츠 분류 ====================
private ContentType contentType;
private Platform platform;
// ==================== 콘텐츠 내용 ====================
private String title;
private String content;
// ==================== 멀티미디어 메타데이터 ====================
@Builder.Default
private List<String> hashtags = new ArrayList<>();
@Builder.Default
private List<String> images = new ArrayList<>();
// ==================== 상태 관리 ====================
private ContentStatus status;
// ==================== 생성 조건 ====================
private CreationConditions creationConditions;
// ==================== 매장 정보 ====================
private Long storeId;
// ==================== 프로모션 기간 ====================
private LocalDateTime promotionStartDate;
private LocalDateTime promotionEndDate;
// ==================== 메타데이터 ====================
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public Content(ContentId of, ContentType contentType, Platform platform, String title, String content, List<String> strings, List<String> strings1, ContentStatus contentStatus, CreationConditions conditions, Long storeId, LocalDateTime createdAt, LocalDateTime updatedAt) {

View File

@ -24,8 +24,6 @@ public class CreationConditions {
private String id;
private String category;
private String requirement;
// private String toneAndManner;
// private String emotionIntensity;
private String storeName;
private String storeType;
private String target;

View File

@ -7,12 +7,17 @@ import com.won.smarketing.content.application.usecase.SnsContentUseCase;
import com.won.smarketing.content.presentation.dto.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
@ -62,23 +67,33 @@ public class ContentController {
* @return 생성된 포스터 콘텐츠 정보
*/
@Operation(summary = "홍보 포스터 생성", description = "AI를 활용하여 홍보 포스터를 생성합니다.")
@PostMapping("/poster/generate")
public ResponseEntity<ApiResponse<PosterContentCreateResponse>> generatePosterContent(@Valid @RequestBody PosterContentCreateRequest request) {
PosterContentCreateResponse response = posterContentUseCase.generatePosterContent(request);
return ResponseEntity.ok(ApiResponse.success(response, "포스터 콘텐츠가 성공적으로 생성되었습니다."));
}
@PostMapping(value = "/poster/generate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<ApiResponse<PosterContentCreateResponse>> generatePosterContent(
@Parameter(
description = "참고할 이미지 파일들 (선택사항, 최대 5개)",
required = false,
content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE)
)
@RequestPart(value = "images", required = false) List<MultipartFile> images,
@Parameter(
description = "포스터 생성 요청 정보",
required = true,
example = """
{
"title": "신메뉴 출시 이벤트",
"category": "이벤트",
"requirement": "밝고 화사한 분위기로 만들어주세요",
"eventName": "아메리카노 할인 이벤트",
"startDate": "2024-01-15",
"endDate": "2024-01-31",
"photoStyle": "밝고 화사한"
}
"""
)
@RequestPart(value = "request") @Valid PosterContentCreateRequest request) {
/**
* 홍보 포스터 저장
*
* @param request 포스터 콘텐츠 저장 요청
* @return 저장 성공 응답
*/
@Operation(summary = "홍보 포스터 저장", description = "생성된 홍보 포스터를 저장합니다.")
@PostMapping("/poster/save")
public ResponseEntity<ApiResponse<Void>> savePosterContent(@Valid @RequestBody PosterContentSaveRequest request) {
posterContentUseCase.savePosterContent(request);
return ResponseEntity.ok(ApiResponse.success(null, "포스터 콘텐츠가 성공적으로 저장되었습니다."));
PosterContentCreateResponse response = posterContentUseCase.generatePosterContent(images, request);
return ResponseEntity.ok(ApiResponse.success(response, "포스터 콘텐츠가 성공적으로 생성되었습니다."));
}
/**

View File

@ -50,9 +50,7 @@ public class PosterContentCreateRequest {
@Schema(description = "이미지 스타일", example = "모던")
private String imageStyle;
@Schema(description = "업로드된 이미지 URL 목록", required = true)
@NotNull(message = "이미지는 1개 이상 필수입니다")
@Size(min = 1, message = "이미지는 1개 이상 업로드해야 합니다")
@Schema(description = "업로드된 이미지 URL 목록")
private List<String> images;
@Schema(description = "콘텐츠 카테고리", example = "이벤트")

View File

@ -1,8 +1,6 @@
// 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;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@ -19,12 +17,7 @@ import java.util.List;
@Schema(description = "포스터 콘텐츠 저장 요청")
public class PosterContentSaveRequest {
// @Schema(description = "콘텐츠 ID", example = "1", required = true)
// @NotNull(message = "콘텐츠 ID는 필수입니다")
// private Long contentId;
@Schema(description = "매장 ID", example = "1", required = true)
@NotNull(message = "매장 ID는 필수입니다")
@Schema(description = "매장 ID", example = "1")
private Long storeId;
@Schema(description = "제목", example = "특별 이벤트 안내")
@ -46,12 +39,6 @@ public class PosterContentSaveRequest {
@Schema(description = "구체적인 요구사항", example = "신메뉴 출시 이벤트 포스터를 만들어주세요")
private String requirement;
@Schema(description = "톤앤매너", example = "전문적")
private String toneAndManner;
@Schema(description = "감정 강도", example = "보통")
private String emotionIntensity;
@Schema(description = "이벤트명", example = "신메뉴 출시 이벤트")
private String eventName;

View File

@ -68,18 +68,6 @@ public class SnsContentCreateRequest {
@Schema(description = "콘텐츠 타입", example = "SNS 게시물")
private String contentType;
// @Schema(description = "톤앤매너",
// example = "친근함",
// allowableValues = {"친근함", "전문적", "유머러스", "감성적", "트렌디"})
// private String toneAndManner;
// @Schema(description = "감정 강도",
// example = "보통",
// allowableValues = {"약함", "보통", "강함"})
// private String emotionIntensity;
// ==================== 이벤트 정보 ====================
@Schema(description = "이벤트명 (이벤트 콘텐츠인 경우)",
example = "신메뉴 출시 이벤트")
@Size(max = 200, message = "이벤트명은 200자 이하로 입력해주세요")