mirror of
https://github.com/won-ktds/smarketing-backend.git
synced 2025-12-06 07:06:24 +00:00
commit
1579194e3e
@ -98,7 +98,7 @@ def create_app():
|
|||||||
app.logger.error(traceback.format_exc())
|
app.logger.error(traceback.format_exc())
|
||||||
return jsonify({'error': f'SNS 콘텐츠 생성 중 오류가 발생했습니다: {str(e)}'}), 500
|
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():
|
def generate_poster_content():
|
||||||
"""
|
"""
|
||||||
홍보 포스터 생성 API
|
홍보 포스터 생성 API
|
||||||
@ -114,7 +114,7 @@ def create_app():
|
|||||||
return jsonify({'error': '요청 데이터가 없습니다.'}), 400
|
return jsonify({'error': '요청 데이터가 없습니다.'}), 400
|
||||||
|
|
||||||
# 필수 필드 검증
|
# 필수 필드 검증
|
||||||
required_fields = ['title', 'category', 'contentType', 'images']
|
required_fields = ['title', 'category', 'images']
|
||||||
for field in required_fields:
|
for field in required_fields:
|
||||||
if field not in data:
|
if field not in data:
|
||||||
return jsonify({'error': f'필수 필드가 누락되었습니다: {field}'}), 400
|
return jsonify({'error': f'필수 필드가 누락되었습니다: {field}'}), 400
|
||||||
@ -140,12 +140,9 @@ def create_app():
|
|||||||
poster_request = PosterContentGetRequest(
|
poster_request = PosterContentGetRequest(
|
||||||
title=data.get('title'),
|
title=data.get('title'),
|
||||||
category=data.get('category'),
|
category=data.get('category'),
|
||||||
contentType=data.get('contentType'),
|
|
||||||
images=data.get('images', []),
|
images=data.get('images', []),
|
||||||
photoStyle=data.get('photoStyle'),
|
photoStyle=data.get('photoStyle'),
|
||||||
requirement=data.get('requirement'),
|
requirement=data.get('requirement'),
|
||||||
toneAndManner=data.get('toneAndManner'),
|
|
||||||
emotionIntensity=data.get('emotionIntensity'),
|
|
||||||
menuName=data.get('menuName'),
|
menuName=data.get('menuName'),
|
||||||
eventName=data.get('eventName'),
|
eventName=data.get('eventName'),
|
||||||
startDate=start_date,
|
startDate=start_date,
|
||||||
|
|||||||
@ -33,12 +33,9 @@ class PosterContentGetRequest:
|
|||||||
"""홍보 포스터 생성 요청 모델"""
|
"""홍보 포스터 생성 요청 모델"""
|
||||||
title: str
|
title: str
|
||||||
category: str
|
category: str
|
||||||
contentType: str
|
|
||||||
images: List[str] # 이미지 URL 리스트
|
images: List[str] # 이미지 URL 리스트
|
||||||
photoStyle: Optional[str] = None
|
photoStyle: Optional[str] = None
|
||||||
requirement: Optional[str] = None
|
requirement: Optional[str] = None
|
||||||
toneAndManner: Optional[str] = None
|
|
||||||
emotionIntensity: Optional[str] = None
|
|
||||||
menuName: Optional[str] = None
|
menuName: Optional[str] = None
|
||||||
eventName: Optional[str] = None
|
eventName: Optional[str] = None
|
||||||
startDate: Optional[date] = None # LocalDate -> date
|
startDate: Optional[date] = None # LocalDate -> date
|
||||||
|
|||||||
@ -154,7 +154,6 @@ class PosterService:
|
|||||||
|
|
||||||
### 📋 기본 정보
|
### 📋 기본 정보
|
||||||
카테고리: {request.category}
|
카테고리: {request.category}
|
||||||
콘텐츠 타입: {request.contentType}
|
|
||||||
메뉴명: {request.menuName or '없음'}
|
메뉴명: {request.menuName or '없음'}
|
||||||
메뉴 정보: {main_description}
|
메뉴 정보: {main_description}
|
||||||
|
|
||||||
|
|||||||
@ -53,6 +53,15 @@ subprojects {
|
|||||||
implementation 'com.azure:azure-messaging-eventhubs-checkpointstore-blob:1.19.0'
|
implementation 'com.azure:azure-messaging-eventhubs-checkpointstore-blob:1.19.0'
|
||||||
implementation 'com.azure:azure-identity:1.11.4'
|
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-core'
|
||||||
|
implementation 'com.fasterxml.jackson.core:jackson-databind'
|
||||||
|
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named('test') {
|
tasks.named('test') {
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':common')
|
implementation project(':common')
|
||||||
runtimeOnly 'org.postgresql:postgresql'
|
runtimeOnly 'org.postgresql:postgresql'
|
||||||
|
|
||||||
// WebClient를 위한 Spring WebFlux 의존성
|
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
|
||||||
}
|
}
|
||||||
@ -8,28 +8,36 @@ import com.won.smarketing.content.domain.model.CreationConditions;
|
|||||||
import com.won.smarketing.content.domain.model.Platform;
|
import com.won.smarketing.content.domain.model.Platform;
|
||||||
import com.won.smarketing.content.domain.repository.ContentRepository;
|
import com.won.smarketing.content.domain.repository.ContentRepository;
|
||||||
import com.won.smarketing.content.domain.service.AiPosterGenerator;
|
import com.won.smarketing.content.domain.service.AiPosterGenerator;
|
||||||
|
import com.won.smarketing.content.domain.service.BlobStorageService;
|
||||||
import com.won.smarketing.content.presentation.dto.PosterContentCreateRequest;
|
import com.won.smarketing.content.presentation.dto.PosterContentCreateRequest;
|
||||||
import com.won.smarketing.content.presentation.dto.PosterContentCreateResponse;
|
import com.won.smarketing.content.presentation.dto.PosterContentCreateResponse;
|
||||||
import com.won.smarketing.content.presentation.dto.PosterContentSaveRequest;
|
import com.won.smarketing.content.presentation.dto.PosterContentSaveRequest;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import 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 java.time.LocalDateTime;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 포스터 콘텐츠 생성
|
* 포스터 콘텐츠 생성
|
||||||
@ -39,26 +47,20 @@ public class PosterContentService implements PosterContentUseCase {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public PosterContentCreateResponse generatePosterContent(PosterContentCreateRequest request) {
|
public PosterContentCreateResponse generatePosterContent(List<MultipartFile> images, PosterContentCreateRequest request) {
|
||||||
|
|
||||||
|
// 1. 이미지 blob storage에 저장하고 request 저장
|
||||||
|
List<String> imageUrls = blobStorageService.uploadImage(images, posterImageContainer);
|
||||||
|
request.setImages(imageUrls);
|
||||||
|
|
||||||
|
// 2. AI 요청
|
||||||
String generatedPoster = aiPosterGenerator.generatePoster(request);
|
String generatedPoster = aiPosterGenerator.generatePoster(request);
|
||||||
|
|
||||||
// 생성 조건 정보 구성
|
|
||||||
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()
|
return PosterContentCreateResponse.builder()
|
||||||
.contentId(null) // 임시 생성이므로 ID 없음
|
.contentId(null) // 임시 생성이므로 ID 없음
|
||||||
.contentType(ContentType.POSTER.name())
|
.contentType(ContentType.POSTER.name())
|
||||||
.title(request.getTitle())
|
.title(request.getTitle())
|
||||||
.posterImage(generatedPoster)
|
.content(generatedPoster)
|
||||||
.posterSizes(new HashMap<>()) // 빈 맵 반환 (사이즈 변환 안함)
|
|
||||||
.status(ContentStatus.DRAFT.name())
|
.status(ContentStatus.DRAFT.name())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
@ -68,9 +70,8 @@ public class PosterContentService implements PosterContentUseCase {
|
|||||||
*
|
*
|
||||||
* @param request 포스터 콘텐츠 저장 요청
|
* @param request 포스터 콘텐츠 저장 요청
|
||||||
*/
|
*/
|
||||||
@Override
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void savePosterContent(PosterContentSaveRequest request) {
|
public Content savePosterContent(PosterContentSaveRequest request) {
|
||||||
// 생성 조건 구성
|
// 생성 조건 구성
|
||||||
CreationConditions conditions = CreationConditions.builder()
|
CreationConditions conditions = CreationConditions.builder()
|
||||||
.category(request.getCategory())
|
.category(request.getCategory())
|
||||||
@ -84,8 +85,9 @@ public class PosterContentService implements PosterContentUseCase {
|
|||||||
// 콘텐츠 엔티티 생성
|
// 콘텐츠 엔티티 생성
|
||||||
Content content = Content.builder()
|
Content content = Content.builder()
|
||||||
.contentType(ContentType.POSTER)
|
.contentType(ContentType.POSTER)
|
||||||
|
.platform(Platform.GENERAL)
|
||||||
.title(request.getTitle())
|
.title(request.getTitle())
|
||||||
.content(request.getContent())
|
// .content(request.gen)
|
||||||
.images(request.getImages())
|
.images(request.getImages())
|
||||||
.status(ContentStatus.PUBLISHED)
|
.status(ContentStatus.PUBLISHED)
|
||||||
.creationConditions(conditions)
|
.creationConditions(conditions)
|
||||||
@ -93,6 +95,6 @@ public class PosterContentService implements PosterContentUseCase {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
// 저장
|
// 저장
|
||||||
contentRepository.save(content);
|
return contentRepository.save(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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.model.Platform;
|
||||||
import com.won.smarketing.content.domain.repository.ContentRepository;
|
import com.won.smarketing.content.domain.repository.ContentRepository;
|
||||||
import com.won.smarketing.content.domain.service.AiContentGenerator;
|
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.SnsContentCreateRequest;
|
||||||
import com.won.smarketing.content.presentation.dto.SnsContentCreateResponse;
|
import com.won.smarketing.content.presentation.dto.SnsContentCreateResponse;
|
||||||
import com.won.smarketing.content.presentation.dto.SnsContentSaveRequest;
|
import com.won.smarketing.content.presentation.dto.SnsContentSaveRequest;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -30,6 +32,7 @@ public class SnsContentService implements SnsContentUseCase {
|
|||||||
|
|
||||||
private final ContentRepository contentRepository;
|
private final ContentRepository contentRepository;
|
||||||
private final AiContentGenerator aiContentGenerator;
|
private final AiContentGenerator aiContentGenerator;
|
||||||
|
private final BlobStorageService blobStorageService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SNS 콘텐츠 생성
|
* SNS 콘텐츠 생성
|
||||||
@ -39,14 +42,17 @@ public class SnsContentService implements SnsContentUseCase {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request) {
|
public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request, List<MultipartFile> files) {
|
||||||
|
//파일들 주소 가져옴
|
||||||
|
List<String> urls = blobStorageService.uploadImage(files, "containerName");
|
||||||
|
request.setImages(urls);
|
||||||
|
|
||||||
// AI를 사용하여 SNS 콘텐츠 생성
|
// AI를 사용하여 SNS 콘텐츠 생성
|
||||||
String content = aiContentGenerator.generateSnsContent(request);
|
String content = aiContentGenerator.generateSnsContent(request);
|
||||||
|
|
||||||
return SnsContentCreateResponse.builder()
|
return SnsContentCreateResponse.builder()
|
||||||
.content(content)
|
.content(content)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
// marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java
|
// marketing-content/src/main/java/com/won/smarketing/content/application/usecase/PosterContentUseCase.java
|
||||||
package com.won.smarketing.content.application.usecase;
|
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.PosterContentCreateRequest;
|
||||||
import com.won.smarketing.content.presentation.dto.PosterContentCreateResponse;
|
import com.won.smarketing.content.presentation.dto.PosterContentCreateResponse;
|
||||||
import com.won.smarketing.content.presentation.dto.PosterContentSaveRequest;
|
import com.won.smarketing.content.presentation.dto.PosterContentSaveRequest;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 포스터 콘텐츠 관련 UseCase 인터페이스
|
* 포스터 콘텐츠 관련 UseCase 인터페이스
|
||||||
@ -16,11 +20,11 @@ public interface PosterContentUseCase {
|
|||||||
* @param request 포스터 콘텐츠 생성 요청
|
* @param request 포스터 콘텐츠 생성 요청
|
||||||
* @return 포스터 콘텐츠 생성 응답
|
* @return 포스터 콘텐츠 생성 응답
|
||||||
*/
|
*/
|
||||||
PosterContentCreateResponse generatePosterContent(PosterContentCreateRequest request);
|
PosterContentCreateResponse generatePosterContent(List<MultipartFile> images, PosterContentCreateRequest request);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 포스터 콘텐츠 저장
|
* 포스터 콘텐츠 저장
|
||||||
* @param request 포스터 콘텐츠 저장 요청
|
* @param request 포스터 콘텐츠 저장 요청
|
||||||
*/
|
*/
|
||||||
void savePosterContent(PosterContentSaveRequest request);
|
Content savePosterContent(PosterContentSaveRequest request);
|
||||||
}
|
}
|
||||||
@ -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.SnsContentCreateRequest;
|
||||||
import com.won.smarketing.content.presentation.dto.SnsContentCreateResponse;
|
import com.won.smarketing.content.presentation.dto.SnsContentCreateResponse;
|
||||||
import com.won.smarketing.content.presentation.dto.SnsContentSaveRequest;
|
import com.won.smarketing.content.presentation.dto.SnsContentSaveRequest;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SNS 콘텐츠 관련 UseCase 인터페이스
|
* SNS 콘텐츠 관련 UseCase 인터페이스
|
||||||
@ -16,7 +19,7 @@ public interface SnsContentUseCase {
|
|||||||
* @param request SNS 콘텐츠 생성 요청
|
* @param request SNS 콘텐츠 생성 요청
|
||||||
* @return SNS 콘텐츠 생성 응답
|
* @return SNS 콘텐츠 생성 응답
|
||||||
*/
|
*/
|
||||||
SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request);
|
SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request, List<MultipartFile> files);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SNS 콘텐츠 저장
|
* SNS 콘텐츠 저장
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,3 @@
|
|||||||
// marketing-content/src/main/java/com/won/smarketing/content/config/WebClientConfig.java
|
|
||||||
package com.won.smarketing.content.config;
|
package com.won.smarketing.content.config;
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
@ -20,8 +19,8 @@ public class WebClientConfig {
|
|||||||
@Bean
|
@Bean
|
||||||
public WebClient webClient() {
|
public WebClient webClient() {
|
||||||
HttpClient httpClient = HttpClient.create()
|
HttpClient httpClient = HttpClient.create()
|
||||||
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
|
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 15000) // 연결 타임아웃: 15초
|
||||||
.responseTimeout(Duration.ofMillis(30000));
|
.responseTimeout(Duration.ofMinutes(5)); // 응답 타임아웃: 5분
|
||||||
|
|
||||||
return WebClient.builder()
|
return WebClient.builder()
|
||||||
.clientConnector(new ReactorClientHttpConnector(httpClient))
|
.clientConnector(new ReactorClientHttpConnector(httpClient))
|
||||||
|
|||||||
@ -27,42 +27,37 @@ import java.util.List;
|
|||||||
@Builder
|
@Builder
|
||||||
public class Content {
|
public class Content {
|
||||||
|
|
||||||
// ==================== 기본키 및 식별자 ====================
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
@Column(name = "content_id")
|
@Column(name = "content_id")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
// ==================== 콘텐츠 분류 ====================
|
|
||||||
private ContentType contentType;
|
private ContentType contentType;
|
||||||
|
|
||||||
private Platform platform;
|
private Platform platform;
|
||||||
|
|
||||||
// ==================== 콘텐츠 내용 ====================
|
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
private String content;
|
private String content;
|
||||||
|
|
||||||
// ==================== 멀티미디어 및 메타데이터 ====================
|
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private List<String> hashtags = new ArrayList<>();
|
private List<String> hashtags = new ArrayList<>();
|
||||||
|
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private List<String> images = new ArrayList<>();
|
private List<String> images = new ArrayList<>();
|
||||||
|
|
||||||
// ==================== 상태 관리 ====================
|
|
||||||
private ContentStatus status;
|
private ContentStatus status;
|
||||||
|
|
||||||
// ==================== 생성 조건 ====================
|
|
||||||
private CreationConditions creationConditions;
|
private CreationConditions creationConditions;
|
||||||
|
|
||||||
// ==================== 매장 정보 ====================
|
|
||||||
private Long storeId;
|
private Long storeId;
|
||||||
|
|
||||||
// ==================== 프로모션 기간 ====================
|
|
||||||
private LocalDateTime promotionStartDate;
|
private LocalDateTime promotionStartDate;
|
||||||
|
|
||||||
private LocalDateTime promotionEndDate;
|
private LocalDateTime promotionEndDate;
|
||||||
|
|
||||||
// ==================== 메타데이터 ====================
|
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
public Content(ContentId of, ContentType contentType, Platform platform, String title, String content, List<String> strings, List<String> strings1, ContentStatus contentStatus, CreationConditions conditions, Long storeId, LocalDateTime createdAt, LocalDateTime updatedAt) {
|
public Content(ContentId of, ContentType contentType, Platform platform, String title, String content, List<String> strings, List<String> strings1, ContentStatus contentStatus, CreationConditions conditions, Long storeId, LocalDateTime createdAt, LocalDateTime updatedAt) {
|
||||||
|
|||||||
@ -24,8 +24,6 @@ public class CreationConditions {
|
|||||||
private String id;
|
private String id;
|
||||||
private String category;
|
private String category;
|
||||||
private String requirement;
|
private String requirement;
|
||||||
// private String toneAndManner;
|
|
||||||
// private String emotionIntensity;
|
|
||||||
private String storeName;
|
private String storeName;
|
||||||
private String storeType;
|
private String storeType;
|
||||||
private String target;
|
private String target;
|
||||||
|
|||||||
@ -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<String> uploadImage(List<MultipartFile> file, String containerName);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 파일 삭제
|
||||||
|
*
|
||||||
|
* @param fileUrl 삭제할 파일의 URL
|
||||||
|
* @return 삭제 성공 여부
|
||||||
|
*/
|
||||||
|
//boolean deleteFile(String fileUrl);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컨테이너 존재 여부 확인 및 생성
|
||||||
|
*
|
||||||
|
* @param containerName 컨테이너 이름
|
||||||
|
*/
|
||||||
|
void ensureContainerExists(String containerName);
|
||||||
|
}
|
||||||
@ -0,0 +1,245 @@
|
|||||||
|
// 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.max-file-size:10485760}") // 10MB
|
||||||
|
private long maxFileSize;
|
||||||
|
|
||||||
|
// 허용되는 이미지 확장자
|
||||||
|
private static final List<String> ALLOWED_EXTENSIONS = Arrays.asList(
|
||||||
|
"jpg", "jpeg", "png", "gif", "bmp", "webp"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 허용되는 MIME 타입
|
||||||
|
private static final List<String> ALLOWED_MIME_TYPES = Arrays.asList(
|
||||||
|
"image/jpeg", "image/png", "image/gif", "image/bmp", "image/webp"
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 파일 업로드
|
||||||
|
*
|
||||||
|
* @param files 업로드할 파일들
|
||||||
|
* @return 업로드된 파일의 URL
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<String> uploadImage(List<MultipartFile> files, String containerName) {
|
||||||
|
// 파일 유효성 검증
|
||||||
|
validateImageFile(files);
|
||||||
|
List<String> 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<MultipartFile> files) {
|
||||||
|
// 파일 존재 여부 확인
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 파일 확장자 확인
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,7 +9,6 @@ import org.springframework.beans.factory.annotation.Value;
|
|||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -44,15 +43,10 @@ public class ClaudeAiContentGenerator implements AiContentGenerator {
|
|||||||
requestBody.put("category", request.getCategory());
|
requestBody.put("category", request.getCategory());
|
||||||
requestBody.put("contentType", request.getContentType());
|
requestBody.put("contentType", request.getContentType());
|
||||||
requestBody.put("requirement", request.getRequirement());
|
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("target", request.getTarget());
|
||||||
|
|
||||||
requestBody.put("event_name", request.getEventName());
|
requestBody.put("event_name", request.getEventName());
|
||||||
requestBody.put("start_date", request.getStartDate());
|
requestBody.put("start_date", request.getStartDate());
|
||||||
requestBody.put("end_date", request.getEndDate());
|
requestBody.put("end_date", request.getEndDate());
|
||||||
|
|
||||||
requestBody.put("images", request.getImages());
|
requestBody.put("images", request.getImages());
|
||||||
|
|
||||||
// Python AI 서비스 호출
|
// Python AI 서비스 호출
|
||||||
@ -63,8 +57,8 @@ public class ClaudeAiContentGenerator implements AiContentGenerator {
|
|||||||
.bodyValue(requestBody)
|
.bodyValue(requestBody)
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.bodyToMono(Map.class)
|
.bodyToMono(Map.class)
|
||||||
.timeout(Duration.ofSeconds(30))
|
.timeout(Duration.ofSeconds(300))
|
||||||
.block();
|
.block(Duration.ofMinutes(6));
|
||||||
|
|
||||||
String content = "";
|
String content = "";
|
||||||
|
|
||||||
@ -76,10 +70,6 @@ public class ClaudeAiContentGenerator implements AiContentGenerator {
|
|||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
return content;
|
return content;
|
||||||
// } catch (Exception e) {
|
|
||||||
// log.error("AI 서비스 호출 실패: {}", e.getMessage(), e);
|
|
||||||
// return generateFallbackContent(request.getTitle(), Platform.fromString(request.getPlatform()));
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -46,12 +46,12 @@ public class PythonAiPosterGenerator implements AiPosterGenerator {
|
|||||||
// Python AI 서비스 호출
|
// Python AI 서비스 호출
|
||||||
Map<String, Object> response = webClient
|
Map<String, Object> response = webClient
|
||||||
.post()
|
.post()
|
||||||
.uri(aiServiceBaseUrl + "/api/ai/poster")
|
.uri("http://localhost:5001" + "/api/ai/poster")
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.bodyValue(requestBody)
|
.bodyValue(requestBody)
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.bodyToMono(Map.class)
|
.bodyToMono(Map.class)
|
||||||
.timeout(Duration.ofSeconds(60)) // 포스터 생성은 시간이 오래 걸릴 수 있음
|
.timeout(Duration.ofSeconds(90)) // 포스터 생성은 시간이 오래 걸릴 수 있음
|
||||||
.block();
|
.block();
|
||||||
|
|
||||||
// 응답에서 content(이미지 URL) 추출
|
// 응답에서 content(이미지 URL) 추출
|
||||||
|
|||||||
@ -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;
|
||||||
@ -7,12 +9,17 @@ import com.won.smarketing.content.application.usecase.SnsContentUseCase;
|
|||||||
import com.won.smarketing.content.presentation.dto.*;
|
import com.won.smarketing.content.presentation.dto.*;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.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.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,6 +27,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
|
||||||
@ -28,17 +36,19 @@ 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 게시물 생성
|
||||||
*
|
|
||||||
* @param request SNS 콘텐츠 생성 요청
|
|
||||||
* @return 생성된 SNS 콘텐츠 정보
|
* @return 생성된 SNS 콘텐츠 정보
|
||||||
*/
|
*/
|
||||||
@Operation(summary = "SNS 게시물 생성", description = "AI를 활용하여 SNS 게시물을 생성합니다.")
|
@Operation(summary = "SNS 게시물 생성", description = "AI를 활용하여 SNS 게시물을 생성합니다.")
|
||||||
@PostMapping("/sns/generate")
|
@PostMapping(path = "/sns/generate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
public ResponseEntity<ApiResponse<SnsContentCreateResponse>> generateSnsContent(@Valid @RequestBody SnsContentCreateRequest request) {
|
public ResponseEntity<ApiResponse<SnsContentCreateResponse>> generateSnsContent(@Valid @RequestPart("request") String requestJson,
|
||||||
SnsContentCreateResponse response = snsContentUseCase.generateSnsContent(request);
|
@Valid @RequestPart("files") List<MultipartFile> images) throws JsonProcessingException {
|
||||||
|
SnsContentCreateRequest request = objectMapper.readValue(requestJson, SnsContentCreateRequest.class);
|
||||||
|
SnsContentCreateResponse response = snsContentUseCase.generateSnsContent(request, images);
|
||||||
return ResponseEntity.ok(ApiResponse.success(response, "SNS 콘텐츠가 성공적으로 생성되었습니다."));
|
return ResponseEntity.ok(ApiResponse.success(response, "SNS 콘텐츠가 성공적으로 생성되었습니다."));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,9 +72,16 @@ public class ContentController {
|
|||||||
* @return 생성된 포스터 콘텐츠 정보
|
* @return 생성된 포스터 콘텐츠 정보
|
||||||
*/
|
*/
|
||||||
@Operation(summary = "홍보 포스터 생성", description = "AI를 활용하여 홍보 포스터를 생성합니다.")
|
@Operation(summary = "홍보 포스터 생성", description = "AI를 활용하여 홍보 포스터를 생성합니다.")
|
||||||
@PostMapping("/poster/generate")
|
@PostMapping(value = "/poster/generate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
public ResponseEntity<ApiResponse<PosterContentCreateResponse>> generatePosterContent(@Valid @RequestBody PosterContentCreateRequest request) {
|
public ResponseEntity<ApiResponse<PosterContentCreateResponse>> generatePosterContent(
|
||||||
PosterContentCreateResponse response = posterContentUseCase.generatePosterContent(request);
|
@Parameter(content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE))
|
||||||
|
@RequestPart(value = "images", required = false) List<MultipartFile> images,
|
||||||
|
@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, "포스터 콘텐츠가 성공적으로 생성되었습니다."));
|
return ResponseEntity.ok(ApiResponse.success(response, "포스터 콘텐츠가 성공적으로 생성되었습니다."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -50,9 +50,7 @@ public class PosterContentCreateRequest {
|
|||||||
@Schema(description = "이미지 스타일", example = "모던")
|
@Schema(description = "이미지 스타일", example = "모던")
|
||||||
private String imageStyle;
|
private String imageStyle;
|
||||||
|
|
||||||
@Schema(description = "업로드된 이미지 URL 목록", required = true)
|
@Schema(description = "업로드된 이미지 URL 목록")
|
||||||
@NotNull(message = "이미지는 1개 이상 필수입니다")
|
|
||||||
@Size(min = 1, message = "이미지는 1개 이상 업로드해야 합니다")
|
|
||||||
private List<String> images;
|
private List<String> images;
|
||||||
|
|
||||||
@Schema(description = "콘텐츠 카테고리", example = "이벤트")
|
@Schema(description = "콘텐츠 카테고리", example = "이벤트")
|
||||||
|
|||||||
@ -31,19 +31,9 @@ public class PosterContentCreateResponse {
|
|||||||
@Schema(description = "생성된 포스터 타입")
|
@Schema(description = "생성된 포스터 타입")
|
||||||
private String contentType;
|
private String contentType;
|
||||||
|
|
||||||
@Schema(description = "포스터 이미지 URL")
|
|
||||||
private String posterImage;
|
|
||||||
|
|
||||||
@Schema(description = "원본 이미지 URL 목록")
|
|
||||||
private List<String> originalImages;
|
|
||||||
|
|
||||||
@Schema(description = "이미지 스타일", example = "모던")
|
@Schema(description = "이미지 스타일", example = "모던")
|
||||||
private String imageStyle;
|
private String imageStyle;
|
||||||
|
|
||||||
@Schema(description = "생성 상태", example = "DRAFT")
|
@Schema(description = "생성 상태", example = "DRAFT")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
@Schema(description = "포스터사이즈", example = "800x600")
|
|
||||||
private Map<String, String> posterSizes;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,8 +1,6 @@
|
|||||||
// smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/presentation/dto/PosterContentSaveRequest.java
|
|
||||||
package com.won.smarketing.content.presentation.dto;
|
package com.won.smarketing.content.presentation.dto;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
@ -19,12 +17,7 @@ import java.util.List;
|
|||||||
@Schema(description = "포스터 콘텐츠 저장 요청")
|
@Schema(description = "포스터 콘텐츠 저장 요청")
|
||||||
public class PosterContentSaveRequest {
|
public class PosterContentSaveRequest {
|
||||||
|
|
||||||
@Schema(description = "콘텐츠 ID", example = "1", required = true)
|
@Schema(description = "매장 ID", example = "1")
|
||||||
@NotNull(message = "콘텐츠 ID는 필수입니다")
|
|
||||||
private Long contentId;
|
|
||||||
|
|
||||||
@Schema(description = "매장 ID", example = "1", required = true)
|
|
||||||
@NotNull(message = "매장 ID는 필수입니다")
|
|
||||||
private Long storeId;
|
private Long storeId;
|
||||||
|
|
||||||
@Schema(description = "제목", example = "특별 이벤트 안내")
|
@Schema(description = "제목", example = "특별 이벤트 안내")
|
||||||
@ -39,19 +32,12 @@ public class PosterContentSaveRequest {
|
|||||||
@Schema(description = "발행 상태", example = "PUBLISHED")
|
@Schema(description = "발행 상태", example = "PUBLISHED")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
// CreationConditions에 필요한 필드들
|
|
||||||
@Schema(description = "콘텐츠 카테고리", example = "이벤트")
|
@Schema(description = "콘텐츠 카테고리", example = "이벤트")
|
||||||
private String category;
|
private String category;
|
||||||
|
|
||||||
@Schema(description = "구체적인 요구사항", example = "신메뉴 출시 이벤트 포스터를 만들어주세요")
|
@Schema(description = "구체적인 요구사항", example = "신메뉴 출시 이벤트 포스터를 만들어주세요")
|
||||||
private String requirement;
|
private String requirement;
|
||||||
|
|
||||||
@Schema(description = "톤앤매너", example = "전문적")
|
|
||||||
private String toneAndManner;
|
|
||||||
|
|
||||||
@Schema(description = "감정 강도", example = "보통")
|
|
||||||
private String emotionIntensity;
|
|
||||||
|
|
||||||
@Schema(description = "이벤트명", example = "신메뉴 출시 이벤트")
|
@Schema(description = "이벤트명", example = "신메뉴 출시 이벤트")
|
||||||
private String eventName;
|
private String eventName;
|
||||||
|
|
||||||
|
|||||||
@ -68,18 +68,6 @@ public class SnsContentCreateRequest {
|
|||||||
@Schema(description = "콘텐츠 타입", example = "SNS 게시물")
|
@Schema(description = "콘텐츠 타입", example = "SNS 게시물")
|
||||||
private String contentType;
|
private String contentType;
|
||||||
|
|
||||||
// @Schema(description = "톤앤매너",
|
|
||||||
// example = "친근함",
|
|
||||||
// allowableValues = {"친근함", "전문적", "유머러스", "감성적", "트렌디"})
|
|
||||||
// private String toneAndManner;
|
|
||||||
|
|
||||||
// @Schema(description = "감정 강도",
|
|
||||||
// example = "보통",
|
|
||||||
// allowableValues = {"약함", "보통", "강함"})
|
|
||||||
// private String emotionIntensity;
|
|
||||||
|
|
||||||
// ==================== 이벤트 정보 ====================
|
|
||||||
|
|
||||||
@Schema(description = "이벤트명 (이벤트 콘텐츠인 경우)",
|
@Schema(description = "이벤트명 (이벤트 콘텐츠인 경우)",
|
||||||
example = "신메뉴 출시 이벤트")
|
example = "신메뉴 출시 이벤트")
|
||||||
@Size(max = 200, message = "이벤트명은 200자 이하로 입력해주세요")
|
@Size(max = 200, message = "이벤트명은 200자 이하로 입력해주세요")
|
||||||
|
|||||||
@ -37,7 +37,15 @@ logging:
|
|||||||
external:
|
external:
|
||||||
ai-service:
|
ai-service:
|
||||||
base-url: ${AI_SERVICE_BASE_URL:http://20.249.113.247:5001}
|
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:
|
management:
|
||||||
endpoints:
|
endpoints:
|
||||||
web:
|
web:
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':common')
|
implementation project(':common')
|
||||||
runtimeOnly 'com.mysql:mysql-connector-j'
|
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'
|
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user