From 1d548a5f5c3ccf76bcfc5e59b4c08bd21585d992 Mon Sep 17 00:00:00 2001 From: yuhalog Date: Tue, 17 Jun 2025 17:26:36 +0900 Subject: [PATCH] feat: poster content --- .../service/PosterContentService.java | 25 +++++++--- .../usecase/PosterContentUseCase.java | 12 ++--- .../content/domain/model/Content.java | 13 ++--- .../domain/model/CreationConditions.java | 2 - .../controller/ContentController.java | 47 ++++++++++++------- .../dto/PosterContentCreateRequest.java | 4 +- .../dto/PosterContentSaveRequest.java | 15 +----- .../dto/SnsContentCreateRequest.java | 12 ----- 8 files changed, 60 insertions(+), 70 deletions(-) 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 69c6213..a2d6cc0 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 @@ -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 images, PosterContentCreateRequest request) { + // 1. 이미지 blob storage에 저장하고 request 저장 + List 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; } } \ No newline at end of file diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java index 6bf2960..101482b 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java @@ -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 images, PosterContentCreateRequest request); - /** - * 포스터 콘텐츠 저장 - * @param request 포스터 콘텐츠 저장 요청 - */ - void savePosterContent(PosterContentSaveRequest request); } \ No newline at end of file diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Content.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Content.java index 32b4231..1a453ef 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Content.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/Content.java @@ -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 hashtags = new ArrayList<>(); @Builder.Default private List 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 strings, List strings1, ContentStatus contentStatus, CreationConditions conditions, Long storeId, LocalDateTime createdAt, LocalDateTime updatedAt) { 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 a284c2c..b90959e 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,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; 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 b454fb1..0bd8e61 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 @@ -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> 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> generatePosterContent( + @Parameter( + description = "참고할 이미지 파일들 (선택사항, 최대 5개)", + required = false, + content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE) + ) + @RequestPart(value = "images", required = false) List 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> 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, "포스터 콘텐츠가 성공적으로 생성되었습니다.")); } /** diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentCreateRequest.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentCreateRequest.java index 1cbf87d..65508ce 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentCreateRequest.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentCreateRequest.java @@ -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 images; @Schema(description = "콘텐츠 카테고리", example = "이벤트") diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentSaveRequest.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentSaveRequest.java index 9cdf9e1..f3c5877 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentSaveRequest.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentSaveRequest.java @@ -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; 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 f8bcdeb..271d604 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 @@ -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자 이하로 입력해주세요")