mirror of
https://github.com/won-ktds/smarketing-backend.git
synced 2026-06-13 04:49:10 +00:00
Merge branch 'main' into marketing-contents
This commit is contained in:
+32
-17
@@ -6,30 +6,41 @@ import com.won.smarketing.content.domain.model.ContentStatus;
|
||||
import com.won.smarketing.content.domain.model.ContentType;
|
||||
import com.won.smarketing.content.domain.model.CreationConditions;
|
||||
import com.won.smarketing.content.domain.model.Platform;
|
||||
import com.won.smarketing.content.domain.model.store.StoreWithMenuData;
|
||||
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.domain.service.StoreDataProvider;
|
||||
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.security.core.context.SecurityContextHolder;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 포스터 콘텐츠 서비스 구현체
|
||||
* 홍보 포스터 생성 및 저장 기능 구현
|
||||
*/
|
||||
@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;
|
||||
private final StoreDataProvider storeDataProvider;
|
||||
|
||||
/**
|
||||
* 포스터 콘텐츠 생성
|
||||
@@ -39,26 +50,24 @@ public class PosterContentService implements PosterContentUseCase {
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public PosterContentCreateResponse generatePosterContent(PosterContentCreateRequest request) {
|
||||
public PosterContentCreateResponse generatePosterContent(List<MultipartFile> images, PosterContentCreateRequest request) {
|
||||
|
||||
String generatedPoster = aiPosterGenerator.generatePoster(request);
|
||||
// 1. 이미지 blob storage에 저장하고 request 저장
|
||||
List<String> imageUrls = blobStorageService.uploadImage(images, posterImageContainer);
|
||||
request.setImages(imageUrls);
|
||||
|
||||
// 매장 정보 호출
|
||||
String userId = getCurrentUserId();
|
||||
StoreWithMenuData storeWithMenuData = storeDataProvider.getStoreWithMenuData(userId);
|
||||
|
||||
// 생성 조건 정보 구성
|
||||
CreationConditions conditions = CreationConditions.builder()
|
||||
.category(request.getCategory())
|
||||
.requirement(request.getRequirement())
|
||||
.eventName(request.getEventName())
|
||||
.startDate(request.getStartDate())
|
||||
.endDate(request.getEndDate())
|
||||
.photoStyle(request.getPhotoStyle())
|
||||
.build();
|
||||
// 2. AI 요청
|
||||
String generatedPoster = aiPosterGenerator.generatePoster(request, storeWithMenuData);
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -68,7 +77,6 @@ public class PosterContentService implements PosterContentUseCase {
|
||||
*
|
||||
* @param request 포스터 콘텐츠 저장 요청
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public void savePosterContent(PosterContentSaveRequest request) {
|
||||
// 생성 조건 구성
|
||||
@@ -96,4 +104,11 @@ public class PosterContentService implements PosterContentUseCase {
|
||||
// 저장
|
||||
contentRepository.save(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 로그인된 사용자 ID 조회
|
||||
*/
|
||||
private String getCurrentUserId() {
|
||||
return SecurityContextHolder.getContext().getAuthentication().getName();
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -14,6 +14,7 @@ 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.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -34,6 +35,9 @@ public class SnsContentService implements SnsContentUseCase {
|
||||
private final AiContentGenerator aiContentGenerator;
|
||||
private final BlobStorageService blobStorageService;
|
||||
|
||||
@Value("${azure.storage.container.poster-images:content-images}")
|
||||
private String contentImageContainer;
|
||||
|
||||
/**
|
||||
* SNS 콘텐츠 생성
|
||||
*
|
||||
@@ -44,8 +48,10 @@ public class SnsContentService implements SnsContentUseCase {
|
||||
@Transactional
|
||||
public SnsContentCreateResponse generateSnsContent(SnsContentCreateRequest request, List<MultipartFile> files) {
|
||||
//파일들 주소 가져옴
|
||||
List<String> urls = blobStorageService.uploadImage(files);
|
||||
request.setImages(urls);
|
||||
if(files != null) {
|
||||
List<String> urls = blobStorageService.uploadImage(files, contentImageContainer);
|
||||
request.setImages(urls);
|
||||
}
|
||||
|
||||
// AI를 사용하여 SNS 콘텐츠 생성
|
||||
String content = aiContentGenerator.generateSnsContent(request);
|
||||
|
||||
+5
-1
@@ -1,9 +1,13 @@
|
||||
// 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;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 포스터 콘텐츠 관련 UseCase 인터페이스
|
||||
@@ -16,7 +20,7 @@ public interface PosterContentUseCase {
|
||||
* @param request 포스터 콘텐츠 생성 요청
|
||||
* @return 포스터 콘텐츠 생성 응답
|
||||
*/
|
||||
PosterContentCreateResponse generatePosterContent(PosterContentCreateRequest request);
|
||||
PosterContentCreateResponse generatePosterContent(List<MultipartFile> images, PosterContentCreateRequest request);
|
||||
|
||||
/**
|
||||
* 포스터 콘텐츠 저장
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package com.won.smarketing.content.config;
|
||||
|
||||
import com.won.smarketing.common.security.JwtAuthenticationFilter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Spring Security 설정 클래스
|
||||
* JWT 기반 인증 및 CORS 설정
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig
|
||||
{
|
||||
|
||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
|
||||
@Value("${allowed-origins}")
|
||||
private String allowedOrigins;
|
||||
/**
|
||||
* Spring Security 필터 체인 설정
|
||||
*
|
||||
* @param http HttpSecurity 객체
|
||||
* @return SecurityFilterChain
|
||||
* @throws Exception 예외
|
||||
*/
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/api/auth/**", "/api/member/register", "/api/member/check-duplicate/**",
|
||||
"/api/member/validate-password", "/swagger-ui/**", "/v3/api-docs/**",
|
||||
"/swagger-resources/**", "/webjars/**", "/actuator/**", "/health/**", "/error"
|
||||
).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 패스워드 인코더 빈 등록
|
||||
*
|
||||
* @return BCryptPasswordEncoder
|
||||
*/
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
/**
|
||||
* CORS 설정
|
||||
*
|
||||
* @return CorsConfigurationSource
|
||||
*/
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
configuration.setAllowedOrigins(Arrays.asList(allowedOrigins.split(",")));
|
||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
configuration.setAllowedHeaders(Arrays.asList("*"));
|
||||
configuration.setAllowCredentials(true);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return source;
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -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, 50000)
|
||||
.responseTimeout(Duration.ofMillis(300000));
|
||||
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 15000) // 연결 타임아웃: 15초
|
||||
.responseTimeout(Duration.ofMinutes(5)); // 응답 타임아웃: 5분
|
||||
|
||||
return WebClient.builder()
|
||||
.clientConnector(new ReactorClientHttpConnector(httpClient))
|
||||
|
||||
+4
-9
@@ -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<String> hashtags = new ArrayList<>();
|
||||
|
||||
@Builder.Default
|
||||
private List<String> 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<String> strings, List<String> strings1, ContentStatus contentStatus, CreationConditions conditions, Long storeId, LocalDateTime createdAt, LocalDateTime updatedAt) {
|
||||
|
||||
-2
@@ -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;
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.won.smarketing.content.domain.model.store;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 메뉴 데이터 값 객체
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MenuData {
|
||||
private Long menuId;
|
||||
private String menuName;
|
||||
private String category;
|
||||
private Integer price;
|
||||
private String description;
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.won.smarketing.content.domain.model.store;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 매장 데이터 값 객체
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class StoreData {
|
||||
private Long storeId;
|
||||
private String storeName;
|
||||
private String businessType;
|
||||
private String location;
|
||||
private String description;
|
||||
private Integer seatCount;
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.won.smarketing.content.domain.model.store;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class StoreWithMenuData {
|
||||
private StoreData storeData;
|
||||
private List<MenuData> menuDataList;
|
||||
}
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
package com.won.smarketing.content.domain.service;
|
||||
|
||||
import com.won.smarketing.content.domain.model.store.StoreWithMenuData;
|
||||
import com.won.smarketing.content.presentation.dto.PosterContentCreateRequest;
|
||||
|
||||
import java.util.Map;
|
||||
@@ -16,5 +17,5 @@ public interface AiPosterGenerator {
|
||||
* @param request 포스터 생성 요청
|
||||
* @return 생성된 포스터 이미지 URL
|
||||
*/
|
||||
String generatePoster(PosterContentCreateRequest request);
|
||||
String generatePoster(PosterContentCreateRequest request, StoreWithMenuData storeWithMenuData);
|
||||
}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ public interface BlobStorageService {
|
||||
* @param file 업로드할 파일
|
||||
* @return 업로드된 파일의 URL
|
||||
*/
|
||||
List<String> uploadImage(List<MultipartFile> file);
|
||||
List<String> uploadImage(List<MultipartFile> file, String containerName);
|
||||
|
||||
|
||||
/**
|
||||
|
||||
+8
-14
@@ -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<String> uploadImage(List<MultipartFile> files) {
|
||||
public List<String> uploadImage(List<MultipartFile> files, String containerName) {
|
||||
// 파일 유효성 검증
|
||||
validateImageFile(files);
|
||||
List<String> 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);
|
||||
|
||||
// 파일 업로드 (간단한 방식)
|
||||
@@ -158,12 +152,12 @@ public class BlobStorageServiceImpl implements BlobStorageService {
|
||||
* @param files 검증할 파일
|
||||
*/
|
||||
private void validateImageFile(List<MultipartFile> 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);
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package com.won.smarketing.content.domain.service;
|
||||
|
||||
import com.won.smarketing.content.domain.model.store.StoreWithMenuData;
|
||||
|
||||
/**
|
||||
* 매장 데이터 제공 도메인 서비스 인터페이스
|
||||
*/
|
||||
public interface StoreDataProvider {
|
||||
|
||||
StoreWithMenuData getStoreWithMenuData(String userId);
|
||||
}
|
||||
+32
-4
@@ -1,5 +1,8 @@
|
||||
package com.won.smarketing.content.infrastructure.external;
|
||||
|
||||
import com.won.smarketing.content.domain.model.store.MenuData;
|
||||
import com.won.smarketing.content.domain.model.store.StoreData;
|
||||
import com.won.smarketing.content.domain.model.store.StoreWithMenuData;
|
||||
import com.won.smarketing.content.domain.service.AiPosterGenerator; // 도메인 인터페이스 import
|
||||
import com.won.smarketing.content.presentation.dto.PosterContentCreateRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -11,7 +14,9 @@ import org.springframework.web.reactive.function.client.WebClient;
|
||||
import java.time.Duration;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Claude AI를 활용한 포스터 생성 구현체
|
||||
@@ -34,12 +39,12 @@ public class PythonAiPosterGenerator implements AiPosterGenerator {
|
||||
* @return 생성된 포스터 이미지 URL
|
||||
*/
|
||||
@Override
|
||||
public String generatePoster(PosterContentCreateRequest request) {
|
||||
public String generatePoster(PosterContentCreateRequest request, StoreWithMenuData storeWithMenuData) {
|
||||
try {
|
||||
log.info("Python AI 포스터 서비스 호출: {}/api/ai/poster", aiServiceBaseUrl);
|
||||
|
||||
// 요청 데이터 구성
|
||||
Map<String, Object> requestBody = buildRequestBody(request);
|
||||
Map<String, Object> requestBody = buildRequestBody(request, storeWithMenuData);
|
||||
|
||||
log.debug("포스터 생성 요청 데이터: {}", requestBody);
|
||||
|
||||
@@ -51,7 +56,7 @@ public class PythonAiPosterGenerator implements AiPosterGenerator {
|
||||
.bodyValue(requestBody)
|
||||
.retrieve()
|
||||
.bodyToMono(Map.class)
|
||||
.timeout(Duration.ofSeconds(60)) // 포스터 생성은 시간이 오래 걸릴 수 있음
|
||||
.timeout(Duration.ofSeconds(90))
|
||||
.block();
|
||||
|
||||
// 응답에서 content(이미지 URL) 추출
|
||||
@@ -75,9 +80,32 @@ public class PythonAiPosterGenerator implements AiPosterGenerator {
|
||||
* Python 서비스의 PosterContentGetRequest 모델에 맞춤
|
||||
* 카테고리,
|
||||
*/
|
||||
private Map<String, Object> buildRequestBody(PosterContentCreateRequest request) {
|
||||
private Map<String, Object> buildRequestBody(PosterContentCreateRequest request, StoreWithMenuData storeWithMenuData) {
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
|
||||
// TODO : 매장 정보 호출 후 request
|
||||
|
||||
// StoreData storeData = storeWithMenuData.getStoreData();
|
||||
// List<MenuData> menuDataList = storeWithMenuData.getMenuDataList();
|
||||
//
|
||||
// List<Map<String, Object>> menuList = menuDataList.stream()
|
||||
// .map(menu -> {
|
||||
// Map<String, Object> menuMap = new HashMap<>();
|
||||
// menuMap.put("menu_id", menu.getMenuId());
|
||||
// menuMap.put("menu_name", menu.getMenuName());
|
||||
// menuMap.put("category", menu.getCategory());
|
||||
// menuMap.put("price", menu.getPrice());
|
||||
// menuMap.put("description", menu.getDescription());
|
||||
// return menuMap;
|
||||
// })
|
||||
// .collect(Collectors.toList());
|
||||
//
|
||||
// requestBody.put("store_name", storeData.getStoreName());
|
||||
// requestBody.put("business_type", storeData.getBusinessType());
|
||||
// requestBody.put("location", storeData.getLocation());
|
||||
// requestBody.put("seat_count", storeData.getSeatCount());
|
||||
// requestBody.put("menu_list", menuList);
|
||||
|
||||
// 기본 정보
|
||||
requestBody.put("title", request.getTitle());
|
||||
requestBody.put("category", request.getCategory());
|
||||
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
package com.won.smarketing.content.infrastructure.external;
|
||||
|
||||
import com.won.smarketing.common.exception.BusinessException;
|
||||
import com.won.smarketing.common.exception.ErrorCode;
|
||||
import com.won.smarketing.content.domain.model.store.MenuData;
|
||||
import com.won.smarketing.content.domain.model.store.StoreData;
|
||||
import com.won.smarketing.content.domain.model.store.StoreWithMenuData;
|
||||
import com.won.smarketing.content.domain.service.StoreDataProvider;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.client.WebClientException;
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 매장 API 데이터 제공자 구현체
|
||||
*/
|
||||
@Slf4j
|
||||
@Service // 추가된 어노테이션
|
||||
@RequiredArgsConstructor
|
||||
public class StoreApiDataProvider implements StoreDataProvider {
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
@Value("${external.store-service.base-url}")
|
||||
private String storeServiceBaseUrl;
|
||||
|
||||
@Value("${external.store-service.timeout}")
|
||||
private int timeout;
|
||||
|
||||
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||
private static final String BEARER_PREFIX = "Bearer ";
|
||||
|
||||
public StoreWithMenuData getStoreWithMenuData(String userId) {
|
||||
log.info("매장 정보와 메뉴 정보 통합 조회 시작: userId={}", userId);
|
||||
|
||||
try {
|
||||
// 매장 정보와 메뉴 정보를 병렬로 조회
|
||||
StoreData storeData = getStoreDataByUserId(userId);
|
||||
List<MenuData> menuDataList = getMenusByStoreId(storeData.getStoreId());
|
||||
|
||||
StoreWithMenuData result = StoreWithMenuData.builder()
|
||||
.storeData(storeData)
|
||||
.menuDataList(menuDataList)
|
||||
.build();
|
||||
|
||||
log.info("매장 정보와 메뉴 정보 통합 조회 완료: storeId={}, storeName={}, menuCount={}",
|
||||
storeData.getStoreId(), storeData.getStoreName(), menuDataList.size());
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("매장 정보와 메뉴 정보 통합 조회 실패, Mock 데이터 반환: storeId={}", userId, e);
|
||||
|
||||
// 실패 시 Mock 데이터 반환
|
||||
return StoreWithMenuData.builder()
|
||||
.storeData(createMockStoreData(userId))
|
||||
.menuDataList(createMockMenuData(6L))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
public StoreData getStoreDataByUserId(String userId) {
|
||||
try {
|
||||
log.debug("매장 정보 실시간 조회: userId={}", userId);
|
||||
return callStoreServiceByUserId(userId);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("매장 정보 조회 실패, Mock 데이터 반환: userId={}, error={}", userId, e.getMessage());
|
||||
return createMockStoreData(userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public List<MenuData> getMenusByStoreId(Long storeId) {
|
||||
log.info("매장 메뉴 조회 시작: storeId={}", storeId);
|
||||
|
||||
try {
|
||||
return callMenuService(storeId);
|
||||
} catch (Exception e) {
|
||||
log.error("메뉴 조회 실패, Mock 데이터 반환: storeId={}", storeId, e);
|
||||
return createMockMenuData(storeId);
|
||||
}
|
||||
}
|
||||
|
||||
private StoreData callStoreServiceByUserId(String userId) {
|
||||
|
||||
try {
|
||||
StoreApiResponse response = webClient
|
||||
.get()
|
||||
.uri(storeServiceBaseUrl + "/api/store")
|
||||
.header("Authorization", "Bearer " + getCurrentJwtToken()) // JWT 토큰 추가
|
||||
.retrieve()
|
||||
.bodyToMono(StoreApiResponse.class)
|
||||
.timeout(Duration.ofMillis(timeout))
|
||||
.block();
|
||||
|
||||
log.info("response : {}", response.getData().getStoreName());
|
||||
log.info("response : {}", response.getData().getStoreId());
|
||||
|
||||
if (response != null && response.getData() != null) {
|
||||
StoreApiResponse.StoreInfo storeInfo = response.getData();
|
||||
return StoreData.builder()
|
||||
.storeId(storeInfo.getStoreId())
|
||||
.storeName(storeInfo.getStoreName())
|
||||
.businessType(storeInfo.getBusinessType())
|
||||
.location(storeInfo.getAddress())
|
||||
.description(storeInfo.getDescription())
|
||||
.seatCount(storeInfo.getSeatCount())
|
||||
.build();
|
||||
}
|
||||
} catch (WebClientResponseException e) {
|
||||
if (e.getStatusCode().value() == 404) {
|
||||
throw new BusinessException(ErrorCode.STORE_NOT_FOUND);
|
||||
}
|
||||
log.error("매장 서비스 호출 실패: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return createMockStoreData(userId);
|
||||
}
|
||||
|
||||
private String getCurrentJwtToken() {
|
||||
try {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
|
||||
if (attributes == null) {
|
||||
log.warn("RequestAttributes를 찾을 수 없음 - HTTP 요청 컨텍스트 없음");
|
||||
return null;
|
||||
}
|
||||
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
|
||||
|
||||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
|
||||
String token = bearerToken.substring(BEARER_PREFIX.length());
|
||||
log.debug("JWT 토큰 추출 성공: {}...", token.substring(0, Math.min(10, token.length())));
|
||||
return token;
|
||||
} else {
|
||||
log.warn("Authorization 헤더에서 Bearer 토큰을 찾을 수 없음: {}", bearerToken);
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("JWT 토큰 추출 중 오류 발생: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<MenuData> callMenuService(Long storeId) {
|
||||
try {
|
||||
MenuApiResponse response = webClient
|
||||
.get()
|
||||
.uri(storeServiceBaseUrl + "/api/menu/store/" + storeId)
|
||||
.retrieve()
|
||||
.bodyToMono(MenuApiResponse.class)
|
||||
.timeout(Duration.ofMillis(timeout))
|
||||
.block();
|
||||
|
||||
if (response != null && response.getData() != null && !response.getData().isEmpty()) {
|
||||
List<MenuData> menuDataList = response.getData().stream()
|
||||
.map(this::toMenuData)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
log.info("매장 메뉴 조회 성공: storeId={}, menuCount={}", storeId, menuDataList.size());
|
||||
return menuDataList;
|
||||
}
|
||||
} catch (WebClientResponseException e) {
|
||||
if (e.getStatusCode().value() == 404) {
|
||||
log.warn("매장의 메뉴 정보가 없습니다: storeId={}", storeId);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
log.error("메뉴 서비스 호출 실패: storeId={}, error={}", storeId, e.getMessage());
|
||||
} catch (WebClientException e) {
|
||||
log.error("메뉴 서비스 연결 실패: storeId={}, error={}", storeId, e.getMessage());
|
||||
}
|
||||
|
||||
return createMockMenuData(storeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* MenuResponse를 MenuData로 변환
|
||||
*/
|
||||
private MenuData toMenuData(MenuApiResponse.MenuInfo menuInfo) {
|
||||
return MenuData.builder()
|
||||
.menuId(menuInfo.getMenuId())
|
||||
.menuName(menuInfo.getMenuName())
|
||||
.category(menuInfo.getCategory())
|
||||
.price(menuInfo.getPrice())
|
||||
.description(menuInfo.getDescription())
|
||||
.build();
|
||||
}
|
||||
|
||||
private StoreData createMockStoreData(String userId) {
|
||||
return StoreData.builder()
|
||||
.storeName("테스트 카페 " + userId)
|
||||
.businessType("카페")
|
||||
.location("서울시 강남구")
|
||||
.build();
|
||||
}
|
||||
|
||||
private List<MenuData> createMockMenuData(Long storeId) {
|
||||
log.info("Mock 메뉴 데이터 생성: storeId={}", storeId);
|
||||
|
||||
return List.of(
|
||||
MenuData.builder()
|
||||
.menuId(1L)
|
||||
.menuName("아메리카노")
|
||||
.category("음료")
|
||||
.price(4000)
|
||||
.description("깊고 진한 맛의 아메리카노")
|
||||
.build(),
|
||||
MenuData.builder()
|
||||
.menuId(2L)
|
||||
.menuName("카페라떼")
|
||||
.category("음료")
|
||||
.price(4500)
|
||||
.description("부드러운 우유 거품이 올라간 카페라떼")
|
||||
.build(),
|
||||
MenuData.builder()
|
||||
.menuId(3L)
|
||||
.menuName("치즈케이크")
|
||||
.category("디저트")
|
||||
.price(6000)
|
||||
.description("진한 치즈 맛의 수제 케이크")
|
||||
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
@Getter
|
||||
private static class StoreApiResponse {
|
||||
private int status;
|
||||
private String message;
|
||||
private StoreInfo data;
|
||||
|
||||
public int getStatus() { return status; }
|
||||
public void setStatus(int status) { this.status = status; }
|
||||
public String getMessage() { return message; }
|
||||
public void setMessage(String message) { this.message = message; }
|
||||
public StoreInfo getData() { return data; }
|
||||
public void setData(StoreInfo data) { this.data = data; }
|
||||
|
||||
@Getter
|
||||
static class StoreInfo {
|
||||
private Long storeId;
|
||||
private String storeName;
|
||||
private String businessType;
|
||||
private String address;
|
||||
private String description;
|
||||
private Integer seatCount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu API 응답 DTO (새로 추가)
|
||||
*/
|
||||
private static class MenuApiResponse {
|
||||
private List<MenuInfo> data;
|
||||
private String message;
|
||||
private boolean success;
|
||||
|
||||
public List<MenuInfo> getData() { return data; }
|
||||
public void setData(List<MenuInfo> data) { this.data = data; }
|
||||
public String getMessage() { return message; }
|
||||
public void setMessage(String message) { this.message = message; }
|
||||
public boolean isSuccess() { return success; }
|
||||
public void setSuccess(boolean success) { this.success = success; }
|
||||
|
||||
public static class MenuInfo {
|
||||
private Long menuId;
|
||||
private String menuName;
|
||||
private String category;
|
||||
private Integer price;
|
||||
private String description;
|
||||
private String image;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public Long getMenuId() { return menuId; }
|
||||
public void setMenuId(Long menuId) { this.menuId = menuId; }
|
||||
public String getMenuName() { return menuName; }
|
||||
public void setMenuName(String menuName) { this.menuName = menuName; }
|
||||
public String getCategory() { return category; }
|
||||
public void setCategory(String category) { this.category = category; }
|
||||
public Integer getPrice() { return price; }
|
||||
public void setPrice(Integer price) { this.price = price; }
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
public String getImage() { return image; }
|
||||
public void setImage(String image) { this.image = image; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-9
@@ -9,9 +9,10 @@ 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.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -26,17 +27,16 @@ import java.util.List;
|
||||
* SNS 콘텐츠 생성, 포스터 생성, 콘텐츠 관리 기능 제공
|
||||
*/
|
||||
@Tag(name = "마케팅 콘텐츠 관리", description = "AI 기반 마케팅 콘텐츠 생성 및 관리 API")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/content")
|
||||
@RequiredArgsConstructor
|
||||
public class ContentController {
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
private final SnsContentUseCase snsContentUseCase;
|
||||
private final PosterContentUseCase posterContentUseCase;
|
||||
private final ContentQueryUseCase contentQueryUseCase;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* SNS 게시물 생성
|
||||
@@ -46,7 +46,7 @@ public class ContentController {
|
||||
@Operation(summary = "SNS 게시물 생성", description = "AI를 활용하여 SNS 게시물을 생성합니다.")
|
||||
@PostMapping(path = "/sns/generate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public ResponseEntity<ApiResponse<SnsContentCreateResponse>> generateSnsContent(@Valid @RequestPart("request") String requestJson,
|
||||
@Valid @RequestPart("files") List<MultipartFile> images) throws JsonProcessingException {
|
||||
@Valid @RequestPart(name = "files", required = false) 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 콘텐츠가 성공적으로 생성되었습니다."));
|
||||
@@ -72,15 +72,22 @@ public class ContentController {
|
||||
* @return 생성된 포스터 콘텐츠 정보
|
||||
*/
|
||||
@Operation(summary = "홍보 포스터 생성", description = "AI를 활용하여 홍보 포스터를 생성합니다.")
|
||||
@PostMapping("/poster/generate")
|
||||
public ResponseEntity<ApiResponse<PosterContentCreateResponse>> generatePosterContent(@Valid @RequestBody PosterContentCreateRequest request) {
|
||||
PosterContentCreateResponse response = posterContentUseCase.generatePosterContent(request);
|
||||
@PostMapping(value = "/poster/generate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public ResponseEntity<ApiResponse<PosterContentCreateResponse>> generatePosterContent(
|
||||
@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, "포스터 콘텐츠가 성공적으로 생성되었습니다."));
|
||||
}
|
||||
|
||||
/**
|
||||
* 홍보 포스터 저장
|
||||
*
|
||||
*
|
||||
* @param request 포스터 콘텐츠 저장 요청
|
||||
* @return 저장 성공 응답
|
||||
*/
|
||||
|
||||
+1
-3
@@ -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<String> images;
|
||||
|
||||
@Schema(description = "콘텐츠 카테고리", example = "이벤트")
|
||||
|
||||
-10
@@ -31,19 +31,9 @@ public class PosterContentCreateResponse {
|
||||
@Schema(description = "생성된 포스터 타입")
|
||||
private String contentType;
|
||||
|
||||
@Schema(description = "포스터 이미지 URL")
|
||||
private String posterImage;
|
||||
|
||||
@Schema(description = "원본 이미지 URL 목록")
|
||||
private List<String> originalImages;
|
||||
|
||||
@Schema(description = "이미지 스타일", example = "모던")
|
||||
private String imageStyle;
|
||||
|
||||
@Schema(description = "생성 상태", example = "DRAFT")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "포스터사이즈", example = "800x600")
|
||||
private Map<String, String> posterSizes;
|
||||
|
||||
}
|
||||
+1
-18
@@ -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 = "특별 이벤트 안내")
|
||||
@@ -36,22 +29,12 @@ public class PosterContentSaveRequest {
|
||||
@Schema(description = "선택된 포스터 이미지 URL")
|
||||
private List<String> images;
|
||||
|
||||
@Schema(description = "발행 상태", example = "PUBLISHED")
|
||||
private String status;
|
||||
|
||||
// CreationConditions에 필요한 필드들
|
||||
@Schema(description = "콘텐츠 카테고리", example = "이벤트")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "구체적인 요구사항", example = "신메뉴 출시 이벤트 포스터를 만들어주세요")
|
||||
private String requirement;
|
||||
|
||||
@Schema(description = "톤앤매너", example = "전문적")
|
||||
private String toneAndManner;
|
||||
|
||||
@Schema(description = "감정 강도", example = "보통")
|
||||
private String emotionIntensity;
|
||||
|
||||
@Schema(description = "이벤트명", example = "신메뉴 출시 이벤트")
|
||||
private String eventName;
|
||||
|
||||
|
||||
-12
@@ -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자 이하로 입력해주세요")
|
||||
|
||||
@@ -37,6 +37,10 @@ logging:
|
||||
external:
|
||||
ai-service:
|
||||
base-url: ${AI_SERVICE_BASE_URL:http://20.249.113.247:5001}
|
||||
store-service:
|
||||
base-url: ${STORE_SERVICE_URL:http://smarketing.20.249.184.228.nip.io}
|
||||
timeout: ${STORE_SERVICE_TIMEOUT:5000}
|
||||
|
||||
azure:
|
||||
storage:
|
||||
account-name: ${AZURE_STORAGE_ACCOUNT_NAME:stdigitalgarage02}
|
||||
@@ -67,4 +71,7 @@ info:
|
||||
app:
|
||||
name: ${APP_NAME:smarketing-content}
|
||||
version: "1.0.0-MVP"
|
||||
description: "AI 마케팅 서비스 MVP - content"
|
||||
description: "AI 마케팅 서비스 MVP - content"
|
||||
|
||||
|
||||
allowed-origins: ${ALLOWED_ORIGINS:http://localhost:3000}
|
||||
Reference in New Issue
Block a user