diff --git a/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java b/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java index dcc1f20..8bc7076 100644 --- a/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java +++ b/smarketing-java/common/src/main/java/com/won/smarketing/common/config/SecurityConfig.java @@ -76,7 +76,7 @@ public class SecurityConfig { 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; diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/PosterContentService.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/PosterContentService.java index 043a8b2..94c894d 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/PosterContentService.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/application/service/PosterContentService.java @@ -6,15 +6,18 @@ 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; @@ -37,6 +40,7 @@ public class PosterContentService implements PosterContentUseCase { private final ContentRepository contentRepository; private final AiPosterGenerator aiPosterGenerator; private final BlobStorageService blobStorageService; + private final StoreDataProvider storeDataProvider; /** * 포스터 콘텐츠 생성 @@ -51,9 +55,13 @@ public class PosterContentService implements PosterContentUseCase { // 1. 이미지 blob storage에 저장하고 request 저장 List imageUrls = blobStorageService.uploadImage(images, posterImageContainer); request.setImages(imageUrls); + + // 매장 정보 호출 + String userId = getCurrentUserId(); + StoreWithMenuData storeWithMenuData = storeDataProvider.getStoreWithMenuData(userId); // 2. AI 요청 - String generatedPoster = aiPosterGenerator.generatePoster(request); + String generatedPoster = aiPosterGenerator.generatePoster(request, storeWithMenuData); return PosterContentCreateResponse.builder() .contentId(null) // 임시 생성이므로 ID 없음 @@ -96,4 +104,11 @@ public class PosterContentService implements PosterContentUseCase { // 저장 contentRepository.save(content); } + + /** + * 현재 로그인된 사용자 ID 조회 + */ + private String getCurrentUserId() { + return SecurityContextHolder.getContext().getAuthentication().getName(); + } } \ No newline at end of file diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/store/MenuData.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/store/MenuData.java new file mode 100644 index 0000000..d6597ad --- /dev/null +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/store/MenuData.java @@ -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; +} diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/store/StoreData.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/store/StoreData.java new file mode 100644 index 0000000..8ae13f4 --- /dev/null +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/store/StoreData.java @@ -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; +} diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/store/StoreWithMenuData.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/store/StoreWithMenuData.java new file mode 100644 index 0000000..962969b --- /dev/null +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/model/store/StoreWithMenuData.java @@ -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 menuDataList; +} \ No newline at end of file diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/AiPosterGenerator.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/AiPosterGenerator.java index a689d30..c550b6c 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/AiPosterGenerator.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/AiPosterGenerator.java @@ -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); } diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/StoreDataProvider.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/StoreDataProvider.java new file mode 100644 index 0000000..c28d33a --- /dev/null +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/domain/service/StoreDataProvider.java @@ -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); +} diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/PythonAiPosterGenerator.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/PythonAiPosterGenerator.java index 4ea396a..1220a5e 100644 --- a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/PythonAiPosterGenerator.java +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/PythonAiPosterGenerator.java @@ -1,5 +1,6 @@ package com.won.smarketing.content.infrastructure.external; +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; @@ -34,12 +35,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 requestBody = buildRequestBody(request); + Map requestBody = buildRequestBody(request, storeWithMenuData); log.debug("포스터 생성 요청 데이터: {}", requestBody); @@ -75,7 +76,7 @@ public class PythonAiPosterGenerator implements AiPosterGenerator { * Python 서비스의 PosterContentGetRequest 모델에 맞춤 * 카테고리, */ - private Map buildRequestBody(PosterContentCreateRequest request) { + private Map buildRequestBody(PosterContentCreateRequest request, StoreWithMenuData storeWithMenuData) { Map requestBody = new HashMap<>(); // 기본 정보 diff --git a/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/StoreApiDataProvider.java b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/StoreApiDataProvider.java new file mode 100644 index 0000000..8480161 --- /dev/null +++ b/smarketing-java/marketing-content/src/main/java/com/won/smarketing/content/infrastructure/external/StoreApiDataProvider.java @@ -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 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 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 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 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 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 data; + private String message; + private boolean success; + + public List getData() { return data; } + public void setData(List 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; } + } + } +} \ No newline at end of file diff --git a/smarketing-java/member/src/main/resources/application.yml b/smarketing-java/member/src/main/resources/application.yml index 92741bc..46d38e4 100644 --- a/smarketing-java/member/src/main/resources/application.yml +++ b/smarketing-java/member/src/main/resources/application.yml @@ -53,4 +53,4 @@ info: app: name: ${APP_NAME:smarketing-member} version: "1.0.0-MVP" - description: "AI 마케팅 서비스 MVP - member" \ No newline at end of file + description: "AI 마케팅 서비스 MVP - member"