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.model.Platform;
import com.won.smarketing.content.domain.repository.ContentRepository; import com.won.smarketing.content.domain.repository.ContentRepository;
import com.won.smarketing.content.domain.service.AiPosterGenerator; 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.PosterContentCreateRequest;
import com.won.smarketing.content.presentation.dto.PosterContentCreateResponse; import com.won.smarketing.content.presentation.dto.PosterContentCreateResponse;
import com.won.smarketing.content.presentation.dto.PosterContentSaveRequest; import com.won.smarketing.content.presentation.dto.PosterContentSaveRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime;
import java.util.HashMap; 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 ContentRepository contentRepository;
private final AiPosterGenerator aiPosterGenerator; private final AiPosterGenerator aiPosterGenerator;
private final BlobStorageService blobStorageService;
/** /**
* 포스터 콘텐츠 생성 * 포스터 콘텐츠 생성
@ -39,10 +41,18 @@ public class PosterContentService implements PosterContentUseCase {
*/ */
@Override @Override
@Transactional @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); String generatedPoster = aiPosterGenerator.generatePoster(request);
// 3. 저장
Content savedContent = savePosterContent(request, generatedPoster);
// 생성 조건 정보 구성 // 생성 조건 정보 구성
CreationConditions conditions = CreationConditions.builder() CreationConditions conditions = CreationConditions.builder()
.category(request.getCategory()) .category(request.getCategory())
@ -68,9 +78,8 @@ public class PosterContentService implements PosterContentUseCase {
* *
* @param request 포스터 콘텐츠 저장 요청 * @param request 포스터 콘텐츠 저장 요청
*/ */
@Override
@Transactional @Transactional
public void savePosterContent(PosterContentSaveRequest request) { public Content savePosterContent(PosterContentCreateRequest request, String generatedPoster) {
// 생성 조건 구성 // 생성 조건 구성
CreationConditions conditions = CreationConditions.builder() CreationConditions conditions = CreationConditions.builder()
.category(request.getCategory()) .category(request.getCategory())
@ -86,7 +95,7 @@ public class PosterContentService implements PosterContentUseCase {
.contentType(ContentType.POSTER) .contentType(ContentType.POSTER)
.platform(Platform.GENERAL) .platform(Platform.GENERAL)
.title(request.getTitle()) .title(request.getTitle())
.content(request.getContent()) .content(generatedPoster)
.images(request.getImages()) .images(request.getImages())
.status(ContentStatus.PUBLISHED) .status(ContentStatus.PUBLISHED)
.creationConditions(conditions) .creationConditions(conditions)
@ -94,6 +103,8 @@ public class PosterContentService implements PosterContentUseCase {
.build(); .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.PosterContentCreateRequest;
import com.won.smarketing.content.presentation.dto.PosterContentCreateResponse; import com.won.smarketing.content.presentation.dto.PosterContentCreateResponse;
import com.won.smarketing.content.presentation.dto.PosterContentSaveRequest; import com.won.smarketing.content.presentation.dto.PosterContentSaveRequest;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/** /**
* 포스터 콘텐츠 관련 UseCase 인터페이스 * 포스터 콘텐츠 관련 UseCase 인터페이스
@ -12,15 +15,10 @@ import com.won.smarketing.content.presentation.dto.PosterContentSaveRequest;
public interface PosterContentUseCase { public interface PosterContentUseCase {
/** /**
* 포스터 콘텐츠 생성 * 포스터 콘텐츠 생성 저장
* @param request 포스터 콘텐츠 생성 요청 * @param request 포스터 콘텐츠 생성 요청
* @return 포스터 콘텐츠 생성 응답 * @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 @Builder
public class Content { public class Content {
// ==================== 기본키 식별자 ====================
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "content_id") @Column(name = "content_id")
private Long id; private Long id;
// ==================== 콘텐츠 분류 ====================
private ContentType contentType; private ContentType contentType;
private Platform platform; private Platform platform;
// ==================== 콘텐츠 내용 ====================
private String title; private String title;
private String content; private String content;
// ==================== 멀티미디어 메타데이터 ====================
@Builder.Default @Builder.Default
private List<String> hashtags = new ArrayList<>(); private List<String> hashtags = new ArrayList<>();
@Builder.Default @Builder.Default
private List<String> images = new ArrayList<>(); private List<String> images = new ArrayList<>();
// ==================== 상태 관리 ====================
private ContentStatus status; private ContentStatus status;
// ==================== 생성 조건 ====================
private CreationConditions creationConditions; private CreationConditions creationConditions;
// ==================== 매장 정보 ====================
private Long storeId; private Long storeId;
// ==================== 프로모션 기간 ====================
private LocalDateTime promotionStartDate; private LocalDateTime promotionStartDate;
private LocalDateTime promotionEndDate; private LocalDateTime promotionEndDate;
// ==================== 메타데이터 ====================
private LocalDateTime createdAt; private LocalDateTime createdAt;
private LocalDateTime updatedAt; 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) { 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 id;
private String category; private String category;
private String requirement; private String requirement;
// private String toneAndManner;
// private String emotionIntensity;
private String storeName; private String storeName;
private String storeType; private String storeType;
private String target; 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 com.won.smarketing.content.presentation.dto.*;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; 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 io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import org.springframework.web.multipart.MultipartFile;
import java.util.List; import java.util.List;
/** /**
@ -62,23 +67,33 @@ public class ContentController {
* @return 생성된 포스터 콘텐츠 정보 * @return 생성된 포스터 콘텐츠 정보
*/ */
@Operation(summary = "홍보 포스터 생성", description = "AI를 활용하여 홍보 포스터를 생성합니다.") @Operation(summary = "홍보 포스터 생성", description = "AI를 활용하여 홍보 포스터를 생성합니다.")
@PostMapping("/poster/generate") @PostMapping(value = "/poster/generate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<ApiResponse<PosterContentCreateResponse>> generatePosterContent(@Valid @RequestBody PosterContentCreateRequest request) { public ResponseEntity<ApiResponse<PosterContentCreateResponse>> generatePosterContent(
PosterContentCreateResponse response = posterContentUseCase.generatePosterContent(request); @Parameter(
return ResponseEntity.ok(ApiResponse.success(response, "포스터 콘텐츠가 성공적으로 생성되었습니다.")); 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) {
/** PosterContentCreateResponse response = posterContentUseCase.generatePosterContent(images, request);
* 홍보 포스터 저장 return ResponseEntity.ok(ApiResponse.success(response, "포스터 콘텐츠가 성공적으로 생성되었습니다."));
*
* @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, "포스터 콘텐츠가 성공적으로 저장되었습니다."));
} }
/** /**

View File

@ -50,9 +50,7 @@ public class PosterContentCreateRequest {
@Schema(description = "이미지 스타일", example = "모던") @Schema(description = "이미지 스타일", example = "모던")
private String imageStyle; private String imageStyle;
@Schema(description = "업로드된 이미지 URL 목록", required = true) @Schema(description = "업로드된 이미지 URL 목록")
@NotNull(message = "이미지는 1개 이상 필수입니다")
@Size(min = 1, message = "이미지는 1개 이상 업로드해야 합니다")
private List<String> images; private List<String> images;
@Schema(description = "콘텐츠 카테고리", example = "이벤트") @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; package com.won.smarketing.content.presentation.dto;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@ -19,12 +17,7 @@ import java.util.List;
@Schema(description = "포스터 콘텐츠 저장 요청") @Schema(description = "포스터 콘텐츠 저장 요청")
public class PosterContentSaveRequest { public class PosterContentSaveRequest {
// @Schema(description = "콘텐츠 ID", example = "1", required = true) @Schema(description = "매장 ID", example = "1")
// @NotNull(message = "콘텐츠 ID는 필수입니다")
// private Long contentId;
@Schema(description = "매장 ID", example = "1", required = true)
@NotNull(message = "매장 ID는 필수입니다")
private Long storeId; private Long storeId;
@Schema(description = "제목", example = "특별 이벤트 안내") @Schema(description = "제목", example = "특별 이벤트 안내")
@ -46,12 +39,6 @@ public class PosterContentSaveRequest {
@Schema(description = "구체적인 요구사항", example = "신메뉴 출시 이벤트 포스터를 만들어주세요") @Schema(description = "구체적인 요구사항", example = "신메뉴 출시 이벤트 포스터를 만들어주세요")
private String requirement; private String requirement;
@Schema(description = "톤앤매너", example = "전문적")
private String toneAndManner;
@Schema(description = "감정 강도", example = "보통")
private String emotionIntensity;
@Schema(description = "이벤트명", example = "신메뉴 출시 이벤트") @Schema(description = "이벤트명", example = "신메뉴 출시 이벤트")
private String eventName; private String eventName;

View File

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