feat: generate poster

This commit is contained in:
yuhalog 2025-06-18 09:50:11 +09:00
parent 4b7f6c11e6
commit ffa4a7f0a9
5 changed files with 24 additions and 35 deletions

View File

@ -13,6 +13,8 @@ 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 lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
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 org.springframework.web.multipart.MultipartFile;
@ -25,10 +27,14 @@ import java.util.List;
* 홍보 포스터 생성 저장 기능 구현 * 홍보 포스터 생성 저장 기능 구현
*/ */
@Service @Service
@Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
@Transactional(readOnly = true) @Transactional(readOnly = true)
public class PosterContentService implements PosterContentUseCase { public class PosterContentService implements PosterContentUseCase {
@Value("${azure.storage.container.poster-images:poster-images}")
private String posterImageContainer;
private final ContentRepository contentRepository; private final ContentRepository contentRepository;
private final AiPosterGenerator aiPosterGenerator; private final AiPosterGenerator aiPosterGenerator;
private final BlobStorageService blobStorageService; private final BlobStorageService blobStorageService;
@ -42,11 +48,11 @@ public class PosterContentService implements PosterContentUseCase {
@Override @Override
@Transactional @Transactional
public PosterContentCreateResponse generatePosterContent(List<MultipartFile> images, PosterContentCreateRequest request) { public PosterContentCreateResponse generatePosterContent(List<MultipartFile> images, PosterContentCreateRequest request) {
log.info("지점1-1");
// 1. 이미지 blob storage에 저장하고 request 저장 // 1. 이미지 blob storage에 저장하고 request 저장
List<String> imageUrls = blobStorageService.uploadImage(images, "poster-content-original"); List<String> imageUrls = blobStorageService.uploadImage(images, posterImageContainer);
request.setImages(imageUrls); request.setImages(imageUrls);
log.info("지점2-1");
// 2. AI 요청 // 2. AI 요청
String generatedPoster = aiPosterGenerator.generatePoster(request); String generatedPoster = aiPosterGenerator.generatePoster(request);

View File

@ -44,7 +44,7 @@ public class SnsContentService implements SnsContentUseCase {
@Transactional @Transactional
public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request, List<MultipartFile> files) { public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request, List<MultipartFile> files) {
//파일들 주소 가져옴 //파일들 주소 가져옴
List<String> urls = blobStorageService.uploadImage(files); List<String> urls = blobStorageService.uploadImage(files, "containerName");
request.setImages(urls); request.setImages(urls);
// AI를 사용하여 SNS 콘텐츠 생성 // AI를 사용하여 SNS 콘텐츠 생성

View File

@ -17,7 +17,7 @@ public interface BlobStorageService {
* @param file 업로드할 파일 * @param file 업로드할 파일
* @return 업로드된 파일의 URL * @return 업로드된 파일의 URL
*/ */
List<String> uploadImage(List<MultipartFile> file); List<String> uploadImage(List<MultipartFile> file, String containerName);
/** /**

View File

@ -34,12 +34,6 @@ public class BlobStorageServiceImpl implements BlobStorageService {
private final BlobServiceClient blobServiceClient; private final BlobServiceClient blobServiceClient;
@Value("${azure.storage.container.poster-images:poster-images}")
private String posterImageContainer;
@Value("${azure.storage.container.content-images:content-images}")
private String contentImageContainer;
@Value("${azure.storage.max-file-size:10485760}") // 10MB @Value("${azure.storage.max-file-size:10485760}") // 10MB
private long maxFileSize; private long maxFileSize;
@ -60,7 +54,7 @@ public class BlobStorageServiceImpl implements BlobStorageService {
* @return 업로드된 파일의 URL * @return 업로드된 파일의 URL
*/ */
@Override @Override
public List<String> uploadImage(List<MultipartFile> files) { public List<String> uploadImage(List<MultipartFile> files, String containerName) {
// 파일 유효성 검증 // 파일 유효성 검증
validateImageFile(files); validateImageFile(files);
List<String> urls = new ArrayList<>(); List<String> urls = new ArrayList<>();
@ -70,10 +64,10 @@ public class BlobStorageServiceImpl implements BlobStorageService {
for(MultipartFile file : files) { for(MultipartFile file : files) {
String fileName = generateMenuImageFileName(file.getOriginalFilename()); String fileName = generateMenuImageFileName(file.getOriginalFilename());
ensureContainerExists(posterImageContainer); ensureContainerExists(containerName);
// Blob 클라이언트 생성 // Blob 클라이언트 생성
BlobContainerClient containerClient = blobServiceClient.getBlobContainerClient(posterImageContainer); BlobContainerClient containerClient = blobServiceClient.getBlobContainerClient(containerName);
BlobClient blobClient = containerClient.getBlobClient(fileName); BlobClient blobClient = containerClient.getBlobClient(fileName);
// 파일 업로드 (간단한 방식) // 파일 업로드 (간단한 방식)

View File

@ -1,5 +1,7 @@
package com.won.smarketing.content.presentation.controller; package com.won.smarketing.content.presentation.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.won.smarketing.common.dto.ApiResponse; import com.won.smarketing.common.dto.ApiResponse;
import com.won.smarketing.content.application.usecase.ContentQueryUseCase; import com.won.smarketing.content.application.usecase.ContentQueryUseCase;
import com.won.smarketing.content.application.usecase.PosterContentUseCase; import com.won.smarketing.content.application.usecase.PosterContentUseCase;
@ -11,6 +13,7 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponses; 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 lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType; 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.*;
@ -25,6 +28,7 @@ import java.util.List;
* SNS 콘텐츠 생성, 포스터 생성, 콘텐츠 관리 기능 제공 * SNS 콘텐츠 생성, 포스터 생성, 콘텐츠 관리 기능 제공
*/ */
@Tag(name = "마케팅 콘텐츠 관리", description = "AI 기반 마케팅 콘텐츠 생성 및 관리 API") @Tag(name = "마케팅 콘텐츠 관리", description = "AI 기반 마케팅 콘텐츠 생성 및 관리 API")
@Slf4j
@RestController @RestController
@RequestMapping("/api/content") @RequestMapping("/api/content")
@RequiredArgsConstructor @RequiredArgsConstructor
@ -33,6 +37,7 @@ public class ContentController {
private final SnsContentUseCase snsContentUseCase; private final SnsContentUseCase snsContentUseCase;
private final PosterContentUseCase posterContentUseCase; private final PosterContentUseCase posterContentUseCase;
private final ContentQueryUseCase contentQueryUseCase; private final ContentQueryUseCase contentQueryUseCase;
private final ObjectMapper objectMapper;
/** /**
* SNS 게시물 생성 * SNS 게시물 생성
@ -70,28 +75,12 @@ public class ContentController {
@Operation(summary = "홍보 포스터 생성", description = "AI를 활용하여 홍보 포스터를 생성합니다.") @Operation(summary = "홍보 포스터 생성", description = "AI를 활용하여 홍보 포스터를 생성합니다.")
@PostMapping(value = "/poster/generate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @PostMapping(value = "/poster/generate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<ApiResponse<PosterContentCreateResponse>> generatePosterContent( public ResponseEntity<ApiResponse<PosterContentCreateResponse>> generatePosterContent(
@Parameter( @Parameter(content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE))
description = "참고할 이미지 파일들 (선택사항, 최대 5개)",
required = false,
content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE)
)
@RequestPart(value = "images", required = false) List<MultipartFile> images, @RequestPart(value = "images", required = false) List<MultipartFile> images,
@Parameter( @RequestPart("request") String requestJson) throws JsonProcessingException {
description = "포스터 생성 요청 정보",
required = true, // JSON 파싱
example = """ PosterContentCreateRequest request = objectMapper.readValue(requestJson, PosterContentCreateRequest.class);
{
"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); PosterContentCreateResponse response = posterContentUseCase.generatePosterContent(images, request);
return ResponseEntity.ok(ApiResponse.success(response, "포스터 콘텐츠가 성공적으로 생성되었습니다.")); return ResponseEntity.ok(ApiResponse.success(response, "포스터 콘텐츠가 성공적으로 생성되었습니다."));