From 264d8b26fe82b06e7212738ecec22d130cda018c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Tue, 17 Jun 2025 16:45:06 +0900 Subject: [PATCH 01/11] add blob --- .../service/PosterContentService.java | 1 + .../domain/service/BlobStorageService.java | 37 +++ .../service/BlobStorageServiceImpl.java | 252 ++++++++++++++++++ .../dto/PosterContentSaveRequest.java | 6 +- 4 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageService.java create mode 100644 smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageServiceImpl.java 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 55a1b19..69c6213 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 @@ -84,6 +84,7 @@ public class PosterContentService implements PosterContentUseCase { // 콘텐츠 엔티티 생성 Content content = Content.builder() .contentType(ContentType.POSTER) + .platform(Platform.GENERAL) .title(request.getTitle()) .content(request.getContent()) .images(request.getImages()) diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageService.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageService.java new file mode 100644 index 0000000..92a6daf --- /dev/null +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageService.java @@ -0,0 +1,37 @@ +// store/src/main/java/com/won/smarketing/store/service/BlobStorageService.java +package com.won.smarketing.content.domain.service; + +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * Azure Blob Storage 서비스 인터페이스 + * 파일 업로드, 다운로드, 삭제 기능 정의 + */ +public interface BlobStorageService { + + /** + * 이미지 파일 업로드 + * + * @param file 업로드할 파일 + * @return 업로드된 파일의 URL + */ + List uploadImage(List file, String containerName); + + + /** + * 파일 삭제 + * + * @param fileUrl 삭제할 파일의 URL + * @return 삭제 성공 여부 + */ + //boolean deleteFile(String fileUrl); + + /** + * 컨테이너 존재 여부 확인 및 생성 + * + * @param containerName 컨테이너 이름 + */ + void ensureContainerExists(String containerName); +} diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageServiceImpl.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageServiceImpl.java new file mode 100644 index 0000000..fc35673 --- /dev/null +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageServiceImpl.java @@ -0,0 +1,252 @@ +// store/src/main/java/com/won/smarketing/store/service/BlobStorageServiceImpl.java +package com.won.smarketing.content.domain.service; + +import com.azure.core.util.BinaryData; +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.models.BlobHttpHeaders; +import com.azure.storage.blob.models.PublicAccessType; +import com.won.smarketing.common.exception.BusinessException; +import com.won.smarketing.common.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +/** + * Azure Blob Storage 서비스 구현체 + * 이미지 파일 업로드, 삭제 기능 구현 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class BlobStorageServiceImpl implements BlobStorageService { + + 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 + private long maxFileSize; + + // 허용되는 이미지 확장자 + private static final List ALLOWED_EXTENSIONS = Arrays.asList( + "jpg", "jpeg", "png", "gif", "bmp", "webp" + ); + + // 허용되는 MIME 타입 + private static final List ALLOWED_MIME_TYPES = Arrays.asList( + "image/jpeg", "image/png", "image/gif", "image/bmp", "image/webp" + ); + + /** + * 이미지 파일 업로드 + * + * @param files 업로드할 파일들 + * @param containerName 컨테이너 이름 + * @return 업로드된 파일의 URL + */ + @Override + public List uploadImage(List files, String containerName) { + // 파일 유효성 검증 + validateImageFile(files); + List urls = new ArrayList<>(); + + try { + // 컨테이너 존재 확인 및 생성 + for(MultipartFile file : files) { + String fileName = generateMenuImageFileName(file.getOriginalFilename()); + + ensureContainerExists(containerName); + + // Blob 클라이언트 생성 + BlobContainerClient containerClient = blobServiceClient.getBlobContainerClient(containerName); + BlobClient blobClient = containerClient.getBlobClient(fileName); + + // 파일 업로드 (간단한 방식) + BinaryData binaryData = BinaryData.fromBytes(file.getBytes()); + + // 파일 업로드 실행 (덮어쓰기 허용) + blobClient.upload(binaryData, true); + + // Content-Type 설정 + BlobHttpHeaders headers = new BlobHttpHeaders().setContentType(file.getContentType()); + blobClient.setHttpHeaders(headers); + + String fileUrl = blobClient.getBlobUrl(); + log.info("이미지 업로드 성공: {}", fileUrl); + + urls.add(fileUrl); + } + return urls; + + } catch (IOException e) { + log.error("이미지 업로드 실패 - 파일 읽기 오류: {}", e.getMessage()); + throw new BusinessException(ErrorCode.FILE_UPLOAD_FAILED); + } catch (Exception e) { + log.error("이미지 업로드 실패: {}", e.getMessage()); + throw new BusinessException(ErrorCode.FILE_UPLOAD_FAILED); + } + } + + /** + * 파일 삭제 + * + * @param fileUrl 삭제할 파일의 URL + */ +// @Override + public void deleteFile(String fileUrl) { + try { + // URL에서 컨테이너명과 파일명 추출 + String[] urlParts = extractContainerAndFileName(fileUrl); + String containerName = urlParts[0]; + String fileName = urlParts[1]; + + BlobContainerClient containerClient = blobServiceClient.getBlobContainerClient(containerName); + BlobClient blobClient = containerClient.getBlobClient(fileName); + + boolean deleted = blobClient.deleteIfExists(); + + if (deleted) { + log.info("파일 삭제 성공: {}", fileUrl); + } else { + log.warn("파일이 존재하지 않음: {}", fileUrl); + } + + } catch (Exception e) { + log.error("파일 삭제 실패: {}", e.getMessage()); + } + } + + /** + * 컨테이너 존재 여부 확인 및 생성 + * + * @param containerName 컨테이너 이름 + */ + @Override + public void ensureContainerExists(String containerName) { + try { + BlobContainerClient containerClient = blobServiceClient.getBlobContainerClient(containerName); + + if (!containerClient.exists()) { + containerClient.createWithResponse(null, PublicAccessType.BLOB, null, null); + log.info("컨테이너 생성 완료: {}", containerName); + } + + } catch (Exception e) { + log.error("컨테이너 생성 실패: {}", e.getMessage()); + throw new BusinessException(ErrorCode.STORAGE_CONTAINER_ERROR); + } + } + + /** + * 이미지 파일 유효성 검증 + * + * @param files 검증할 파일 + */ + private void validateImageFile(List files) { + for (MultipartFile file : files) { + // 파일 존재 여부 확인 + if (file == null || file.isEmpty()) { + throw new BusinessException(ErrorCode.FILE_NOT_FOUND); + } + + // 파일 크기 확인 + if (file.getSize() > maxFileSize) { + throw new BusinessException(ErrorCode.FILE_SIZE_EXCEEDED); + } + + // 파일 확장자 확인 + String originalFilename = file.getOriginalFilename(); + if (originalFilename == null) { + throw new BusinessException(ErrorCode.INVALID_FILE_NAME); + } + + String extension = getFileExtension(originalFilename).toLowerCase(); + if (!ALLOWED_EXTENSIONS.contains(extension)) { + throw new BusinessException(ErrorCode.INVALID_FILE_EXTENSION); + } + + // MIME 타입 확인 + String contentType = file.getContentType(); + if (contentType == null || !ALLOWED_MIME_TYPES.contains(contentType)) { + throw new BusinessException(ErrorCode.INVALID_FILE_TYPE); + } + } + } + + /** + * 메뉴 이미지 파일명 생성 + * + * @param originalFilename 원본 파일명 + * @return 생성된 파일명 + */ + private String generateMenuImageFileName(String originalFilename) { + String extension = getFileExtension(originalFilename); + String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")); + String uuid = UUID.randomUUID().toString().substring(0, 8); + + return String.format("menu_%s_%s.%s", timestamp, uuid, extension); + } + + /** + * 매장 이미지 파일명 생성 + * + * @param storeId 매장 ID + * @param originalFilename 원본 파일명 + * @return 생성된 파일명 + */ + private String generateStoreImageFileName(Long storeId, String originalFilename) { + String extension = getFileExtension(originalFilename); + String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")); + String uuid = UUID.randomUUID().toString().substring(0, 8); + + return String.format("store_%d_%s_%s.%s", storeId, timestamp, uuid, extension); + } + + /** + * 파일 확장자 추출 + * + * @param filename 파일명 + * @return 확장자 + */ + private String getFileExtension(String filename) { + int lastDotIndex = filename.lastIndexOf('.'); + if (lastDotIndex == -1) { + return ""; + } + return filename.substring(lastDotIndex + 1); + } + + /** + * URL에서 컨테이너명과 파일명 추출 + * + * @param fileUrl 파일 URL + * @return [컨테이너명, 파일명] 배열 + */ + private String[] extractContainerAndFileName(String fileUrl) { + // URL 형식: https://accountname.blob.core.windows.net/container/filename + try { + String[] parts = fileUrl.split("/"); + String containerName = parts[parts.length - 2]; + String fileName = parts[parts.length - 1]; + return new String[]{containerName, fileName}; + } catch (Exception e) { + throw new BusinessException(ErrorCode.INVALID_FILE_URL); + } + } +} \ No newline at end of file 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 5335d11..9cdf9e1 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 @@ -19,9 +19,9 @@ 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는 필수입니다") +// private Long contentId; @Schema(description = "매장 ID", example = "1", required = true) @NotNull(message = "매장 ID는 필수입니다") From a7f5f61726cc9426995b121a7dfe292c22f26d08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Tue, 17 Jun 2025 17:25:56 +0900 Subject: [PATCH 02/11] add blob --- .../service/SnsContentService.java | 10 ++- .../usecase/SnsContentUseCase.java | 5 +- .../config/AzureBlobStorageConfig.java | 72 +++++++++++++++++++ .../domain/service/BlobStorageService.java | 2 +- .../service/BlobStorageServiceImpl.java | 7 +- .../controller/ContentController.java | 10 ++- .../src/main/resources/application.yml | 10 ++- 7 files changed, 104 insertions(+), 12 deletions(-) create mode 100644 smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/AzureBlobStorageConfig.java diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java index 074b39b..6119226 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java @@ -9,12 +9,14 @@ 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.AiContentGenerator; +import com.won.smarketing.content.domain.service.BlobStorageService; import com.won.smarketing.content.presentation.dto.SnsContentCreateRequest; import com.won.smarketing.content.presentation.dto.SnsContentCreateResponse; import com.won.smarketing.content.presentation.dto.SnsContentSaveRequest; 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.List; @@ -30,6 +32,7 @@ public class SnsContentService implements SnsContentUseCase { private final ContentRepository contentRepository; private final AiContentGenerator aiContentGenerator; + private final BlobStorageService blobStorageService; /** * SNS 콘텐츠 생성 @@ -39,14 +42,17 @@ public class SnsContentService implements SnsContentUseCase { */ @Override @Transactional - public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request) { + public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request, List files) { + //파일들 주소 가져옴 + List urls = blobStorageService.uploadImage(files); + request.setImages(urls); + // AI를 사용하여 SNS 콘텐츠 생성 String content = aiContentGenerator.generateSnsContent(request); return SnsContentCreateResponse.builder() .content(content) .build(); - } /** diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/SnsContentUseCase.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/SnsContentUseCase.java index d2c6751..23c23f1 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/SnsContentUseCase.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/usecase/SnsContentUseCase.java @@ -4,6 +4,9 @@ package com.won.smarketing.content.application.usecase; import com.won.smarketing.content.presentation.dto.SnsContentCreateRequest; import com.won.smarketing.content.presentation.dto.SnsContentCreateResponse; import com.won.smarketing.content.presentation.dto.SnsContentSaveRequest; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; /** * SNS 콘텐츠 관련 UseCase 인터페이스 @@ -16,7 +19,7 @@ public interface SnsContentUseCase { * @param request SNS 콘텐츠 생성 요청 * @return SNS 콘텐츠 생성 응답 */ - SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request); + SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request, List files); /** * SNS 콘텐츠 저장 diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/AzureBlobStorageConfig.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/AzureBlobStorageConfig.java new file mode 100644 index 0000000..09d27bd --- /dev/null +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/AzureBlobStorageConfig.java @@ -0,0 +1,72 @@ +// store/src/main/java/com/won/smarketing/store/config/AzureBlobStorageConfig.java +package com.won.smarketing.content.config; + +import com.azure.identity.DefaultAzureCredentialBuilder; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.BlobServiceClientBuilder; +import com.azure.storage.common.StorageSharedKeyCredential; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Azure Blob Storage 설정 클래스 + * Azure Blob Storage와의 연결을 위한 설정 + */ +@Configuration +@Slf4j +public class AzureBlobStorageConfig { + + @Value("${azure.storage.account-name}") + private String accountName; + + @Value("${azure.storage.account-key:}") + private String accountKey; + + @Value("${azure.storage.endpoint:}") + private String endpoint; + + /** + * Azure Blob Storage Service Client 생성 + * + * @return BlobServiceClient 인스턴스 + */ + @Bean + public BlobServiceClient blobServiceClient() { + try { + // Managed Identity 사용 시 (Azure 환경에서 권장) + if (accountKey == null || accountKey.isEmpty()) { + log.info("Azure Blob Storage 연결 - Managed Identity 사용"); + return new BlobServiceClientBuilder() + .endpoint(getEndpoint()) + .credential(new DefaultAzureCredentialBuilder().build()) + .buildClient(); + } + + // Account Key 사용 시 (개발 환경용) + log.info("Azure Blob Storage 연결 - Account Key 사용"); + StorageSharedKeyCredential credential = new StorageSharedKeyCredential(accountName, accountKey); + return new BlobServiceClientBuilder() + .endpoint(getEndpoint()) + .credential(credential) + .buildClient(); + + } catch (Exception e) { + log.error("Azure Blob Storage 클라이언트 생성 실패", e); + throw new RuntimeException("Azure Blob Storage 연결 실패", e); + } + } + + /** + * Storage Account 엔드포인트 URL 생성 + * + * @return 엔드포인트 URL + */ + private String getEndpoint() { + if (endpoint != null && !endpoint.isEmpty()) { + return endpoint; + } + return String.format("https://%s.blob.core.windows.net", accountName); + } +} \ No newline at end of file diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageService.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageService.java index 92a6daf..76ea929 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageService.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageService.java @@ -17,7 +17,7 @@ public interface BlobStorageService { * @param file 업로드할 파일 * @return 업로드된 파일의 URL */ - List uploadImage(List file, String containerName); + List uploadImage(List file); /** diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageServiceImpl.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageServiceImpl.java index fc35673..3bb63f1 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageServiceImpl.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageServiceImpl.java @@ -57,11 +57,10 @@ public class BlobStorageServiceImpl implements BlobStorageService { * 이미지 파일 업로드 * * @param files 업로드할 파일들 - * @param containerName 컨테이너 이름 * @return 업로드된 파일의 URL */ @Override - public List uploadImage(List files, String containerName) { + public List uploadImage(List files) { // 파일 유효성 검증 validateImageFile(files); List urls = new ArrayList<>(); @@ -71,10 +70,10 @@ public class BlobStorageServiceImpl implements BlobStorageService { for(MultipartFile file : files) { String fileName = generateMenuImageFileName(file.getOriginalFilename()); - ensureContainerExists(containerName); + ensureContainerExists(posterImageContainer); // Blob 클라이언트 생성 - BlobContainerClient containerClient = blobServiceClient.getBlobContainerClient(containerName); + BlobContainerClient containerClient = blobServiceClient.getBlobContainerClient(posterImageContainer); BlobClient blobClient = containerClient.getBlobClient(fileName); // 파일 업로드 (간단한 방식) 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..eba9ed8 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 @@ -9,10 +9,13 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; 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; /** @@ -36,9 +39,10 @@ public class ContentController { * @return 생성된 SNS 콘텐츠 정보 */ @Operation(summary = "SNS 게시물 생성", description = "AI를 활용하여 SNS 게시물을 생성합니다.") - @PostMapping("/sns/generate") - public ResponseEntity> generateSnsContent(@Valid @RequestBody SnsContentCreateRequest request) { - SnsContentCreateResponse response = snsContentUseCase.generateSnsContent(request); + @PostMapping(path = "/sns/generate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity> generateSnsContent(@Valid @RequestPart SnsContentCreateRequest request, + @RequestPart("files") List images) { + SnsContentCreateResponse response = snsContentUseCase.generateSnsContent(request, images); return ResponseEntity.ok(ApiResponse.success(response, "SNS 콘텐츠가 성공적으로 생성되었습니다.")); } diff --git a/smarketing-java/marketing-content/src/main/resources/application.yml b/smarketing-java/marketing-content/src/main/resources/application.yml index 871f8d8..819d127 100644 --- a/smarketing-java/marketing-content/src/main/resources/application.yml +++ b/smarketing-java/marketing-content/src/main/resources/application.yml @@ -37,7 +37,15 @@ logging: external: ai-service: base-url: ${AI_SERVICE_BASE_URL:http://20.249.113.247:5001} - +azure: + storage: + account-name: ${AZURE_STORAGE_ACCOUNT_NAME:stdigitalgarage02} + account-key: ${AZURE_STORAGE_ACCOUNT_KEY:} + endpoint: ${AZURE_STORAGE_ENDPOINT:https://stdigitalgarage02.blob.core.windows.net} + container: + menu-images: ${AZURE_STORAGE_MENU_CONTAINER:smarketing-menu-images} + store-images: ${AZURE_STORAGE_STORE_CONTAINER:smarketing-store-images} + max-file-size: ${AZURE_STORAGE_MAX_FILE_SIZE:10485760} # 10MB management: endpoints: web: From 1d548a5f5c3ccf76bcfc5e59b4c08bd21585d992 Mon Sep 17 00:00:00 2001 From: yuhalog Date: Tue, 17 Jun 2025 17:26:36 +0900 Subject: [PATCH 03/11] 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자 이하로 입력해주세요") From 81ddc643e5198b4101c7c028ca2a68f6c5eec3b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Wed, 18 Jun 2025 09:40:03 +0900 Subject: [PATCH 04/11] add requestpart --- .../external/ClaudeAiContentGenerator.java | 2 +- .../presentation/controller/ContentController.java | 14 ++++++++++---- smarketing-java/store/build.gradle | 2 ++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java index 07b0602..7243c26 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java @@ -63,7 +63,7 @@ public class ClaudeAiContentGenerator implements AiContentGenerator { .bodyValue(requestBody) .retrieve() .bodyToMono(Map.class) - .timeout(Duration.ofSeconds(30)) + .timeout(Duration.ofSeconds(60)) .block(); String content = ""; 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 eba9ed8..fd40ffd 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 @@ -1,5 +1,7 @@ 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.content.application.usecase.ContentQueryUseCase; import com.won.smarketing.content.application.usecase.PosterContentUseCase; @@ -9,6 +11,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -28,20 +31,23 @@ import java.util.List; @RequiredArgsConstructor public class ContentController { + @Autowired + private ObjectMapper objectMapper; + private final SnsContentUseCase snsContentUseCase; private final PosterContentUseCase posterContentUseCase; private final ContentQueryUseCase contentQueryUseCase; /** * SNS 게시물 생성 - * - * @param request SNS 콘텐츠 생성 요청 + * @return 생성된 SNS 콘텐츠 정보 */ @Operation(summary = "SNS 게시물 생성", description = "AI를 활용하여 SNS 게시물을 생성합니다.") @PostMapping(path = "/sns/generate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity> generateSnsContent(@Valid @RequestPart SnsContentCreateRequest request, - @RequestPart("files") List images) { + public ResponseEntity> generateSnsContent(@Valid @RequestPart("request") String requestJson, + @Valid @RequestPart("files") List images) throws JsonProcessingException { + SnsContentCreateRequest request = objectMapper.readValue(requestJson, SnsContentCreateRequest.class); SnsContentCreateResponse response = snsContentUseCase.generateSnsContent(request, images); return ResponseEntity.ok(ApiResponse.success(response, "SNS 콘텐츠가 성공적으로 생성되었습니다.")); } diff --git a/smarketing-java/store/build.gradle b/smarketing-java/store/build.gradle index dd9e26d..ef65d80 100644 --- a/smarketing-java/store/build.gradle +++ b/smarketing-java/store/build.gradle @@ -5,4 +5,6 @@ dependencies { // Azure Blob Storage 의존성 추가 implementation 'com.azure:azure-storage-blob:12.25.0' implementation 'com.azure:azure-identity:1.11.1' + + implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.3' } \ No newline at end of file From ffa4a7f0a96e1bc89d42c5b9e9416b892af8a5e5 Mon Sep 17 00:00:00 2001 From: yuhalog Date: Wed, 18 Jun 2025 09:50:11 +0900 Subject: [PATCH 05/11] feat: generate poster --- .../service/PosterContentService.java | 12 +++++-- .../service/SnsContentService.java | 2 +- .../domain/service/BlobStorageService.java | 2 +- .../service/BlobStorageServiceImpl.java | 12 ++----- .../controller/ContentController.java | 31 ++++++------------- 5 files changed, 24 insertions(+), 35 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 a2d6cc0..4af4ddc 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 @@ -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.PosterContentSaveRequest; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -25,10 +27,14 @@ import java.util.List; * 홍보 포스터 생성 및 저장 기능 구현 */ @Service +@Slf4j @RequiredArgsConstructor @Transactional(readOnly = true) public class PosterContentService implements PosterContentUseCase { + @Value("${azure.storage.container.poster-images:poster-images}") + private String posterImageContainer; + private final ContentRepository contentRepository; private final AiPosterGenerator aiPosterGenerator; private final BlobStorageService blobStorageService; @@ -42,11 +48,11 @@ public class PosterContentService implements PosterContentUseCase { @Override @Transactional public PosterContentCreateResponse generatePosterContent(List images, PosterContentCreateRequest request) { - + log.info("지점1-1"); // 1. 이미지 blob storage에 저장하고 request 저장 - List imageUrls = blobStorageService.uploadImage(images, "poster-content-original"); + List imageUrls = blobStorageService.uploadImage(images, posterImageContainer); request.setImages(imageUrls); - + log.info("지점2-1"); // 2. AI 요청 String generatedPoster = aiPosterGenerator.generatePoster(request); diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java index 6119226..233bd5d 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/SnsContentService.java @@ -44,7 +44,7 @@ public class SnsContentService implements SnsContentUseCase { @Transactional public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request, List files) { //파일들 주소 가져옴 - List urls = blobStorageService.uploadImage(files); + List urls = blobStorageService.uploadImage(files, "containerName"); request.setImages(urls); // AI를 사용하여 SNS 콘텐츠 생성 diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageService.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageService.java index 76ea929..92a6daf 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageService.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageService.java @@ -17,7 +17,7 @@ public interface BlobStorageService { * @param file 업로드할 파일 * @return 업로드된 파일의 URL */ - List uploadImage(List file); + List uploadImage(List file, String containerName); /** diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageServiceImpl.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageServiceImpl.java index 3bb63f1..d37a0ea 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageServiceImpl.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageServiceImpl.java @@ -34,12 +34,6 @@ public class BlobStorageServiceImpl implements BlobStorageService { 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 private long maxFileSize; @@ -60,7 +54,7 @@ public class BlobStorageServiceImpl implements BlobStorageService { * @return 업로드된 파일의 URL */ @Override - public List uploadImage(List files) { + public List uploadImage(List files, String containerName) { // 파일 유효성 검증 validateImageFile(files); List urls = new ArrayList<>(); @@ -70,10 +64,10 @@ public class BlobStorageServiceImpl implements BlobStorageService { for(MultipartFile file : files) { String fileName = generateMenuImageFileName(file.getOriginalFilename()); - ensureContainerExists(posterImageContainer); + ensureContainerExists(containerName); // Blob 클라이언트 생성 - BlobContainerClient containerClient = blobServiceClient.getBlobContainerClient(posterImageContainer); + BlobContainerClient containerClient = blobServiceClient.getBlobContainerClient(containerName); BlobClient blobClient = containerClient.getBlobClient(fileName); // 파일 업로드 (간단한 방식) 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 d83f488..48a516b 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 @@ -1,5 +1,7 @@ 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.content.application.usecase.ContentQueryUseCase; 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.tags.Tag; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -25,6 +28,7 @@ import java.util.List; * SNS 콘텐츠 생성, 포스터 생성, 콘텐츠 관리 기능 제공 */ @Tag(name = "마케팅 콘텐츠 관리", description = "AI 기반 마케팅 콘텐츠 생성 및 관리 API") +@Slf4j @RestController @RequestMapping("/api/content") @RequiredArgsConstructor @@ -33,6 +37,7 @@ public class ContentController { private final SnsContentUseCase snsContentUseCase; private final PosterContentUseCase posterContentUseCase; private final ContentQueryUseCase contentQueryUseCase; + private final ObjectMapper objectMapper; /** * SNS 게시물 생성 @@ -70,28 +75,12 @@ public class ContentController { @Operation(summary = "홍보 포스터 생성", description = "AI를 활용하여 홍보 포스터를 생성합니다.") @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) - ) + @Parameter(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) { + @RequestPart("request") String requestJson) throws JsonProcessingException { + + // JSON 파싱 + PosterContentCreateRequest request = objectMapper.readValue(requestJson, PosterContentCreateRequest.class); PosterContentCreateResponse response = posterContentUseCase.generatePosterContent(images, request); return ResponseEntity.ok(ApiResponse.success(response, "포스터 콘텐츠가 성공적으로 생성되었습니다.")); From ee9736bb9f111743a0756dd1ded50618ffa1b50d Mon Sep 17 00:00:00 2001 From: yuhalog Date: Wed, 18 Jun 2025 09:56:48 +0900 Subject: [PATCH 06/11] =?UTF-8?q?refactor:=20build.gradle=20=EC=B5=9C?= =?UTF-8?q?=EC=83=81=EB=8B=A8=EC=9C=BC=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- smarketing-java/build.gradle | 7 +++++++ smarketing-java/marketing-content/build.gradle | 3 --- smarketing-java/store/build.gradle | 6 ------ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/smarketing-java/build.gradle b/smarketing-java/build.gradle index e917ca4..3bd1308 100644 --- a/smarketing-java/build.gradle +++ b/smarketing-java/build.gradle @@ -53,6 +53,13 @@ subprojects { implementation 'com.azure:azure-messaging-eventhubs-checkpointstore-blob:1.19.0' implementation 'com.azure:azure-identity:1.11.4' + // Azure Blob Storage 의존성 추가 + implementation 'com.azure:azure-storage-blob:12.25.0' + implementation 'com.azure:azure-identity:1.11.1' + + implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.3' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + } tasks.named('test') { diff --git a/smarketing-java/marketing-content/build.gradle b/smarketing-java/marketing-content/build.gradle index 715bc47..188d7bd 100644 --- a/smarketing-java/marketing-content/build.gradle +++ b/smarketing-java/marketing-content/build.gradle @@ -1,7 +1,4 @@ dependencies { implementation project(':common') runtimeOnly 'org.postgresql:postgresql' - - // WebClient를 위한 Spring WebFlux 의존성 - implementation 'org.springframework.boot:spring-boot-starter-webflux' } \ No newline at end of file diff --git a/smarketing-java/store/build.gradle b/smarketing-java/store/build.gradle index ef65d80..771a2fc 100644 --- a/smarketing-java/store/build.gradle +++ b/smarketing-java/store/build.gradle @@ -1,10 +1,4 @@ dependencies { implementation project(':common') runtimeOnly 'com.mysql:mysql-connector-j' - - // Azure Blob Storage 의존성 추가 - implementation 'com.azure:azure-storage-blob:12.25.0' - implementation 'com.azure:azure-identity:1.11.1' - - implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.3' } \ No newline at end of file From 379f690cdce0dce54392da41b974bde90ba12060 Mon Sep 17 00:00:00 2001 From: yuhalog Date: Wed, 18 Jun 2025 10:48:45 +0900 Subject: [PATCH 07/11] refactor: poster content --- smarketing-java/build.gradle | 4 +++- .../content/domain/service/BlobStorageServiceImpl.java | 10 +++++----- .../external/PythonAiPosterGenerator.java | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/smarketing-java/build.gradle b/smarketing-java/build.gradle index 3bd1308..01426b7 100644 --- a/smarketing-java/build.gradle +++ b/smarketing-java/build.gradle @@ -57,7 +57,9 @@ subprojects { implementation 'com.azure:azure-storage-blob:12.25.0' implementation 'com.azure:azure-identity:1.11.1' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.3' + implementation 'com.fasterxml.jackson.core:jackson-core' + implementation 'com.fasterxml.jackson.core:jackson-databind' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' implementation 'org.springframework.boot:spring-boot-starter-webflux' } diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageServiceImpl.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageServiceImpl.java index d37a0ea..c9b9d40 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageServiceImpl.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/BlobStorageServiceImpl.java @@ -152,12 +152,12 @@ public class BlobStorageServiceImpl implements BlobStorageService { * @param files 검증할 파일 */ private void validateImageFile(List files) { - for (MultipartFile file : files) { - // 파일 존재 여부 확인 - if (file == null || file.isEmpty()) { - throw new BusinessException(ErrorCode.FILE_NOT_FOUND); - } + // 파일 존재 여부 확인 + if (files == null || files.isEmpty()) { + throw new BusinessException(ErrorCode.FILE_NOT_FOUND); + } + for (MultipartFile file : files) { // 파일 크기 확인 if (file.getSize() > maxFileSize) { throw new BusinessException(ErrorCode.FILE_SIZE_EXCEEDED); diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/PythonAiPosterGenerator.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/PythonAiPosterGenerator.java index 1fc2020..fc1405c 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/PythonAiPosterGenerator.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/PythonAiPosterGenerator.java @@ -46,7 +46,7 @@ public class PythonAiPosterGenerator implements AiPosterGenerator { // Python AI 서비스 호출 Map response = webClient .post() - .uri(aiServiceBaseUrl + "/api/ai/poster") + .uri("http://localhost:5001" + "/api/ai/poster") .header("Content-Type", "application/json") .bodyValue(requestBody) .retrieve() From 4ba5ee7e5d385892558d9f4ca619d732b2eed572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=9C=EC=9D=80?= <61147083+BangSun98@users.noreply.github.com> Date: Wed, 18 Jun 2025 11:22:49 +0900 Subject: [PATCH 08/11] fix timeout error --- .../smarketing/content/config/WebClientConfig.java | 4 ++-- .../external/ClaudeAiContentGenerator.java | 14 ++------------ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java index 7f7cf08..72e1a78 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java @@ -20,8 +20,8 @@ public class WebClientConfig { @Bean public WebClient webClient() { HttpClient httpClient = HttpClient.create() - .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) - .responseTimeout(Duration.ofMillis(30000)); + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 50000) + .responseTimeout(Duration.ofMillis(300000)); return WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java index 7243c26..bb6abdf 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/ClaudeAiContentGenerator.java @@ -9,7 +9,6 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpMethod; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; - import java.time.Duration; import java.util.HashMap; import java.util.Map; @@ -44,15 +43,10 @@ public class ClaudeAiContentGenerator implements AiContentGenerator { requestBody.put("category", request.getCategory()); requestBody.put("contentType", request.getContentType()); requestBody.put("requirement", request.getRequirement()); - - //requestBody.put("tone_and_manner", request.getToneAndManner()); - // requestBody.put("emotion_intensity", request.getEmotionIntensity()); requestBody.put("target", request.getTarget()); - requestBody.put("event_name", request.getEventName()); requestBody.put("start_date", request.getStartDate()); requestBody.put("end_date", request.getEndDate()); - requestBody.put("images", request.getImages()); // Python AI 서비스 호출 @@ -63,8 +57,8 @@ public class ClaudeAiContentGenerator implements AiContentGenerator { .bodyValue(requestBody) .retrieve() .bodyToMono(Map.class) - .timeout(Duration.ofSeconds(60)) - .block(); + .timeout(Duration.ofSeconds(300)) + .block(Duration.ofMinutes(6)); String content = ""; @@ -76,10 +70,6 @@ public class ClaudeAiContentGenerator implements AiContentGenerator { return content; } return content; -// } catch (Exception e) { -// log.error("AI 서비스 호출 실패: {}", e.getMessage(), e); -// return generateFallbackContent(request.getTitle(), Platform.fromString(request.getPlatform())); -// } } /** From 7da375e038e502498bb36ff0c9afc29a77ae34ef Mon Sep 17 00:00:00 2001 From: John Hanzu Kim Date: Wed, 18 Jun 2025 11:32:59 +0900 Subject: [PATCH 09/11] Update deploy.yaml.template --- .../deployment/deploy.yaml.template | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/smarketing-java/deployment/deploy.yaml.template b/smarketing-java/deployment/deploy.yaml.template index 9b8d945..2a2defd 100644 --- a/smarketing-java/deployment/deploy.yaml.template +++ b/smarketing-java/deployment/deploy.yaml.template @@ -462,6 +462,7 @@ spec: type: ClusterIP --- +# deploy.yaml.template의 Ingress 부분 - 완전한 설정 apiVersion: networking.k8s.io/v1 kind: Ingress metadata: @@ -478,6 +479,7 @@ spec: - host: smarketing.20.249.184.228.nip.io http: paths: + # Member 서비스 - 인증 관련 - path: /api/auth(/|$)(.*) pathType: ImplementationSpecific backend: @@ -485,6 +487,15 @@ spec: name: member port: number: 80 + # Member 서비스 - 회원 관리 (누락된 경로!) + - path: /api/member(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: member + port: + number: 80 + # Store 서비스 - path: /api/store(/|$)(.*) pathType: ImplementationSpecific backend: @@ -492,6 +503,31 @@ spec: name: store port: number: 80 + # Store 서비스 - 매출 관련 + - path: /api/sales(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: store + port: + number: 80 + # Store 서비스 - 메뉴 관련 + - path: /api/menu(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: store + port: + number: 80 + # Store 서비스 - 이미지 업로드 + - path: /api/images(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: store + port: + number: 80 + # Marketing Content 서비스 - path: /api/content(/|$)(.*) pathType: ImplementationSpecific backend: @@ -499,6 +535,7 @@ spec: name: marketing-content port: number: 80 + # AI Recommend 서비스 - path: /api/recommend(/|$)(.*) pathType: ImplementationSpecific backend: From 9108232c5a5013f18be285506a51b7196c31416a Mon Sep 17 00:00:00 2001 From: yuhalog Date: Wed, 18 Jun 2025 11:33:53 +0900 Subject: [PATCH 10/11] feat: ai-poster content --- smarketing-ai/app.py | 7 ++----- smarketing-ai/models/request_models.py | 3 --- smarketing-ai/services/poster_service.py | 1 - 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/smarketing-ai/app.py b/smarketing-ai/app.py index d3c91da..e96f33a 100644 --- a/smarketing-ai/app.py +++ b/smarketing-ai/app.py @@ -98,7 +98,7 @@ def create_app(): app.logger.error(traceback.format_exc()) return jsonify({'error': f'SNS 콘텐츠 생성 중 오류가 발생했습니다: {str(e)}'}), 500 - @app.route('/api/ai/poster', methods=['GET']) + @app.route('/api/ai/poster', methods=['POST']) def generate_poster_content(): """ 홍보 포스터 생성 API @@ -114,7 +114,7 @@ def create_app(): return jsonify({'error': '요청 데이터가 없습니다.'}), 400 # 필수 필드 검증 - required_fields = ['title', 'category', 'contentType', 'images'] + required_fields = ['title', 'category', 'images'] for field in required_fields: if field not in data: return jsonify({'error': f'필수 필드가 누락되었습니다: {field}'}), 400 @@ -140,12 +140,9 @@ def create_app(): poster_request = PosterContentGetRequest( title=data.get('title'), category=data.get('category'), - contentType=data.get('contentType'), images=data.get('images', []), photoStyle=data.get('photoStyle'), requirement=data.get('requirement'), - toneAndManner=data.get('toneAndManner'), - emotionIntensity=data.get('emotionIntensity'), menuName=data.get('menuName'), eventName=data.get('eventName'), startDate=start_date, diff --git a/smarketing-ai/models/request_models.py b/smarketing-ai/models/request_models.py index 3f6952d..b21a4e1 100644 --- a/smarketing-ai/models/request_models.py +++ b/smarketing-ai/models/request_models.py @@ -33,12 +33,9 @@ class PosterContentGetRequest: """홍보 포스터 생성 요청 모델""" title: str category: str - contentType: str images: List[str] # 이미지 URL 리스트 photoStyle: Optional[str] = None requirement: Optional[str] = None - toneAndManner: Optional[str] = None - emotionIntensity: Optional[str] = None menuName: Optional[str] = None eventName: Optional[str] = None startDate: Optional[date] = None # LocalDate -> date diff --git a/smarketing-ai/services/poster_service.py b/smarketing-ai/services/poster_service.py index c90119c..3dec55e 100644 --- a/smarketing-ai/services/poster_service.py +++ b/smarketing-ai/services/poster_service.py @@ -154,7 +154,6 @@ class PosterService: ### 📋 기본 정보 카테고리: {request.category} - 콘텐츠 타입: {request.contentType} 메뉴명: {request.menuName or '없음'} 메뉴 정보: {main_description} From b2a40bcee896f4d5ef40d10383081beb2a1b5884 Mon Sep 17 00:00:00 2001 From: yuhalog Date: Wed, 18 Jun 2025 11:34:54 +0900 Subject: [PATCH 11/11] =?UTF-8?q?refactor:=20webClientConfig=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/PosterContentService.java | 28 ++++--------------- .../usecase/PosterContentUseCase.java | 8 +++++- .../content/config/WebClientConfig.java | 5 ++-- .../external/PythonAiPosterGenerator.java | 2 +- .../controller/ContentController.java | 13 +++++++++ .../dto/PosterContentCreateResponse.java | 10 ------- .../dto/PosterContentSaveRequest.java | 1 - 7 files changed, 29 insertions(+), 38 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 4af4ddc..94c5362 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 @@ -48,33 +48,19 @@ public class PosterContentService implements PosterContentUseCase { @Override @Transactional public PosterContentCreateResponse generatePosterContent(List images, PosterContentCreateRequest request) { - log.info("지점1-1"); + // 1. 이미지 blob storage에 저장하고 request 저장 List imageUrls = blobStorageService.uploadImage(images, posterImageContainer); request.setImages(imageUrls); - log.info("지점2-1"); + // 2. AI 요청 String generatedPoster = aiPosterGenerator.generatePoster(request); - // 3. 저장 - Content savedContent = savePosterContent(request, generatedPoster); - - // 생성 조건 정보 구성 - CreationConditions conditions = CreationConditions.builder() - .category(request.getCategory()) - .requirement(request.getRequirement()) - .eventName(request.getEventName()) - .startDate(request.getStartDate()) - .endDate(request.getEndDate()) - .photoStyle(request.getPhotoStyle()) - .build(); - return PosterContentCreateResponse.builder() .contentId(null) // 임시 생성이므로 ID 없음 .contentType(ContentType.POSTER.name()) .title(request.getTitle()) - .posterImage(generatedPoster) - .posterSizes(new HashMap<>()) // 빈 맵 반환 (사이즈 변환 안함) + .content(generatedPoster) .status(ContentStatus.DRAFT.name()) .build(); } @@ -85,7 +71,7 @@ public class PosterContentService implements PosterContentUseCase { * @param request 포스터 콘텐츠 저장 요청 */ @Transactional - public Content savePosterContent(PosterContentCreateRequest request, String generatedPoster) { + public Content savePosterContent(PosterContentSaveRequest request) { // 생성 조건 구성 CreationConditions conditions = CreationConditions.builder() .category(request.getCategory()) @@ -101,7 +87,7 @@ public class PosterContentService implements PosterContentUseCase { .contentType(ContentType.POSTER) .platform(Platform.GENERAL) .title(request.getTitle()) - .content(generatedPoster) +// .content(request.gen) .images(request.getImages()) .status(ContentStatus.PUBLISHED) .creationConditions(conditions) @@ -109,8 +95,6 @@ public class PosterContentService implements PosterContentUseCase { .build(); // 저장 - Content savedContent = contentRepository.save(content); - - return savedContent; + return contentRepository.save(content); } } \ 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 101482b..7f346e3 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 @@ -1,6 +1,7 @@ // marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java package com.won.smarketing.content.application.usecase; +import com.won.smarketing.content.domain.model.Content; import com.won.smarketing.content.presentation.dto.PosterContentCreateRequest; import com.won.smarketing.content.presentation.dto.PosterContentCreateResponse; import com.won.smarketing.content.presentation.dto.PosterContentSaveRequest; @@ -15,10 +16,15 @@ import java.util.List; public interface PosterContentUseCase { /** - * 포스터 콘텐츠 생성 및 저장 + * 포스터 콘텐츠 생성 * @param request 포스터 콘텐츠 생성 요청 * @return 포스터 콘텐츠 생성 응답 */ PosterContentCreateResponse generatePosterContent(List images, PosterContentCreateRequest request); + /** + * 포스터 콘텐츠 저장 + * @param request 포스터 콘텐츠 저장 요청 + */ + Content savePosterContent(PosterContentSaveRequest request); } \ No newline at end of file diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java index 7f7cf08..8fb41fc 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java @@ -1,4 +1,3 @@ -// marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java package com.won.smarketing.content.config; import org.springframework.context.annotation.Bean; @@ -20,8 +19,8 @@ public class WebClientConfig { @Bean public WebClient webClient() { HttpClient httpClient = HttpClient.create() - .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) - .responseTimeout(Duration.ofMillis(30000)); + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 15000) // 연결 타임아웃: 15초 + .responseTimeout(Duration.ofMinutes(5)); // 응답 타임아웃: 5분 return WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/PythonAiPosterGenerator.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/PythonAiPosterGenerator.java index fc1405c..4ea396a 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/PythonAiPosterGenerator.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/PythonAiPosterGenerator.java @@ -51,7 +51,7 @@ public class PythonAiPosterGenerator implements AiPosterGenerator { .bodyValue(requestBody) .retrieve() .bodyToMono(Map.class) - .timeout(Duration.ofSeconds(60)) // 포스터 생성은 시간이 오래 걸릴 수 있음 + .timeout(Duration.ofSeconds(90)) // 포스터 생성은 시간이 오래 걸릴 수 있음 .block(); // 응답에서 content(이미지 URL) 추출 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 f306abc..ab7afa1 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 @@ -85,6 +85,19 @@ public class ContentController { return ResponseEntity.ok(ApiResponse.success(response, "포스터 콘텐츠가 성공적으로 생성되었습니다.")); } + /** + * 홍보 포스터 저장 + * + * @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, "포스터 콘텐츠가 성공적으로 저장되었습니다.")); + } + /** * 콘텐츠 수정 * diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentCreateResponse.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentCreateResponse.java index 0c02b68..5fa5c53 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentCreateResponse.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentCreateResponse.java @@ -31,19 +31,9 @@ public class PosterContentCreateResponse { @Schema(description = "생성된 포스터 타입") private String contentType; - @Schema(description = "포스터 이미지 URL") - private String posterImage; - - @Schema(description = "원본 이미지 URL 목록") - private List originalImages; - @Schema(description = "이미지 스타일", example = "모던") private String imageStyle; @Schema(description = "생성 상태", example = "DRAFT") private String status; - - @Schema(description = "포스터사이즈", example = "800x600") - private Map posterSizes; - } \ No newline at end of file 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 f3c5877..eb549f0 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 @@ -32,7 +32,6 @@ public class PosterContentSaveRequest { @Schema(description = "발행 상태", example = "PUBLISHED") private String status; - // CreationConditions에 필요한 필드들 @Schema(description = "콘텐츠 카테고리", example = "이벤트") private String category;