diff --git a/src/main/java/com/wl2c/elswhereproductservice/client/analysis/api/AnalysisServiceClient.java b/src/main/java/com/wl2c/elswhereproductservice/client/analysis/api/AnalysisServiceClient.java
new file mode 100644
index 0000000..c8ebb21
--- /dev/null
+++ b/src/main/java/com/wl2c/elswhereproductservice/client/analysis/api/AnalysisServiceClient.java
@@ -0,0 +1,18 @@
+package com.wl2c.elswhereproductservice.client.analysis.api;
+
+import com.wl2c.elswhereproductservice.client.analysis.dto.response.ResponseAIResultDto;
+import com.wl2c.elswhereproductservice.domain.product.model.dto.request.RequestProductIdListDto;
+import jakarta.validation.Valid;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+import java.util.List;
+
+@FeignClient(name = "analysis-service")
+public interface AnalysisServiceClient {
+
+ @PostMapping("/v1/ai/list")
+ List getAIResultList(@Valid @RequestBody RequestProductIdListDto requestProductIdListDto);
+
+}
diff --git a/src/main/java/com/wl2c/elswhereproductservice/client/analysis/dto/response/ResponseAIResultDto.java b/src/main/java/com/wl2c/elswhereproductservice/client/analysis/dto/response/ResponseAIResultDto.java
new file mode 100644
index 0000000..3d67426
--- /dev/null
+++ b/src/main/java/com/wl2c/elswhereproductservice/client/analysis/dto/response/ResponseAIResultDto.java
@@ -0,0 +1,22 @@
+package com.wl2c.elswhereproductservice.client.analysis.dto.response;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Builder;
+import lombok.Getter;
+
+import java.math.BigDecimal;
+
+@Getter
+@Builder
+public class ResponseAIResultDto {
+
+ @Schema(description = "AI 결과 id", example = "1")
+ private final Long AIResultId;
+
+ @Schema(description = "상품 id", example = "1")
+ private final Long productId;
+
+ @Schema(description = "AI가 판단한 스텝다운 상품 안전도", example = "0.76")
+ private final BigDecimal safetyScore;
+
+}
diff --git a/src/main/java/com/wl2c/elswhereproductservice/domain/product/controller/ProductController.java b/src/main/java/com/wl2c/elswhereproductservice/domain/product/controller/ProductController.java
index 2f0a3f3..96640d9 100644
--- a/src/main/java/com/wl2c/elswhereproductservice/domain/product/controller/ProductController.java
+++ b/src/main/java/com/wl2c/elswhereproductservice/domain/product/controller/ProductController.java
@@ -1,6 +1,7 @@
package com.wl2c.elswhereproductservice.domain.product.controller;
import com.wl2c.elswhereproductservice.domain.product.exception.TodayReceivedProductsNotFoundException;
+import com.wl2c.elswhereproductservice.domain.product.model.dto.list.SummarizedOnSaleProductDto;
import com.wl2c.elswhereproductservice.domain.product.model.dto.list.SummarizedProductDto;
import com.wl2c.elswhereproductservice.domain.product.model.dto.list.SummarizedProductForHoldingDto;
import com.wl2c.elswhereproductservice.domain.product.model.dto.request.RequestProductIdListDto;
@@ -50,14 +51,19 @@ public class ProductController {
* 수익률순 : profit
* 청약 마감일순 : deadline
*
+ *
+ *
+ * 스텝다운 유형의 상품에 대해서 AI가 분석한 각 상품의 safetyScore를 제공합니다.
+ * 스텝다운 유형이 아니거나 스텝다운 유형이지만 분석 정보가 없는 경우에는 null 값으로 제공됩니다.
+ *
*
* @param type 정렬 타입
* @return 페이징된 청약 중인 상품 목록
*/
@GetMapping("/on-sale")
- public ResponsePage listByOnSale(@RequestParam(name = "type") String type,
- @ParameterObject Pageable pageable) {
- Page result = productService.listByOnSale(type, pageable);
+ public ResponsePage listByOnSale(@RequestParam(name = "type") String type,
+ @ParameterObject Pageable pageable) {
+ Page result = productService.listByOnSale(type, pageable);
return new ResponsePage<>(result);
}
diff --git a/src/main/java/com/wl2c/elswhereproductservice/domain/product/model/dto/list/SummarizedOnSaleProductDto.java b/src/main/java/com/wl2c/elswhereproductservice/domain/product/model/dto/list/SummarizedOnSaleProductDto.java
new file mode 100644
index 0000000..172e229
--- /dev/null
+++ b/src/main/java/com/wl2c/elswhereproductservice/domain/product/model/dto/list/SummarizedOnSaleProductDto.java
@@ -0,0 +1,57 @@
+package com.wl2c.elswhereproductservice.domain.product.model.dto.list;
+
+import com.querydsl.core.annotations.QueryProjection;
+import com.wl2c.elswhereproductservice.domain.product.model.ProductType;
+import com.wl2c.elswhereproductservice.domain.product.model.entity.Product;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+@Getter
+public class SummarizedOnSaleProductDto {
+
+ @Schema(description = "상품 id", example = "1")
+ private final Long id;
+
+ @Schema(description = "발행 회사", example = "oo투자증권")
+ private final String issuer;
+
+ @Schema(description = "상품명", example = "oo투자증권 99999")
+ private final String name;
+
+ @Schema(description = "상품 유형", example = "STEP_DOWN or LIZARD or MONTHLY_PAYMENT or ETC")
+ private final ProductType productType;
+
+ @Schema(description = "기초자산", example = "KOSPI200 Index / HSCEI Index / S&P500 Index")
+ private final String equities;
+
+ @Schema(description = "수익률", example = "20.55")
+ private final BigDecimal yieldIfConditionsMet;
+
+ @Schema(description = "낙인 값", example = "45, 낙인 값이 없을 시 null return")
+ private final Integer knockIn;
+
+ @Schema(description = "청약 시작일", example = "2024-06-14")
+ private final LocalDate subscriptionStartDate;
+
+ @Schema(description = "청약 마감일", example = "2024-06-21")
+ private final LocalDate subscriptionEndDate;
+
+ @Schema(description = "AI가 판단한 상품 안전도", example = "0.89")
+ private final BigDecimal safetyScore;
+
+ public SummarizedOnSaleProductDto(Product product, BigDecimal safetyScore) {
+ this.id = product.getId();
+ this.issuer = product.getIssuer();
+ this.name = product.getName();
+ this.productType = product.getType();
+ this.equities = product.getEquities();
+ this.yieldIfConditionsMet = product.getYieldIfConditionsMet();
+ this.knockIn = product.getKnockIn();
+ this.subscriptionStartDate = product.getSubscriptionStartDate();
+ this.subscriptionEndDate = product.getSubscriptionEndDate();
+ this.safetyScore = safetyScore;
+ }
+}
diff --git a/src/main/java/com/wl2c/elswhereproductservice/domain/product/model/dto/response/ResponseProductComparisonTargetDto.java b/src/main/java/com/wl2c/elswhereproductservice/domain/product/model/dto/response/ResponseProductComparisonTargetDto.java
index fa9a514..7046ac0 100644
--- a/src/main/java/com/wl2c/elswhereproductservice/domain/product/model/dto/response/ResponseProductComparisonTargetDto.java
+++ b/src/main/java/com/wl2c/elswhereproductservice/domain/product/model/dto/response/ResponseProductComparisonTargetDto.java
@@ -46,7 +46,10 @@ public class ResponseProductComparisonTargetDto {
@Schema(description = "청약 마감일", example = "2024-06-21")
private final LocalDate subscriptionEndDate;
- public ResponseProductComparisonTargetDto(Product product) {
+ @Schema(description = "AI가 판단한 상품 안전도", example = "0.89")
+ private final BigDecimal safetyScore;
+
+ public ResponseProductComparisonTargetDto(Product product, BigDecimal safetyScore) {
this.id = product.getId();
this.issuer = product.getIssuer();
this.name = product.getName();
@@ -59,5 +62,6 @@ public ResponseProductComparisonTargetDto(Product product) {
this.maximumLossRate = product.getMaximumLossRate();
this.subscriptionStartDate = product.getSubscriptionStartDate();
this.subscriptionEndDate = product.getSubscriptionEndDate();
+ this.safetyScore = safetyScore;
}
}
diff --git a/src/main/java/com/wl2c/elswhereproductservice/domain/product/service/ProductService.java b/src/main/java/com/wl2c/elswhereproductservice/domain/product/service/ProductService.java
index 0bf88ea..64b05c1 100644
--- a/src/main/java/com/wl2c/elswhereproductservice/domain/product/service/ProductService.java
+++ b/src/main/java/com/wl2c/elswhereproductservice/domain/product/service/ProductService.java
@@ -1,12 +1,17 @@
package com.wl2c.elswhereproductservice.domain.product.service;
+import com.wl2c.elswhereproductservice.client.analysis.api.AnalysisServiceClient;
+import com.wl2c.elswhereproductservice.client.analysis.dto.response.ResponseAIResultDto;
import com.wl2c.elswhereproductservice.domain.like.service.LikeService;
import com.wl2c.elswhereproductservice.domain.product.exception.NotOnSaleProductException;
import com.wl2c.elswhereproductservice.domain.product.exception.ProductNotFoundException;
import com.wl2c.elswhereproductservice.domain.product.exception.TodayReceivedProductsNotFoundException;
import com.wl2c.elswhereproductservice.domain.product.exception.WrongProductSortTypeException;
+import com.wl2c.elswhereproductservice.domain.product.model.ProductType;
+import com.wl2c.elswhereproductservice.domain.product.model.dto.list.SummarizedOnSaleProductDto;
import com.wl2c.elswhereproductservice.domain.product.model.dto.list.SummarizedProductDto;
import com.wl2c.elswhereproductservice.domain.product.model.dto.list.SummarizedProductForHoldingDto;
+import com.wl2c.elswhereproductservice.domain.product.model.dto.request.RequestProductIdListDto;
import com.wl2c.elswhereproductservice.domain.product.model.dto.request.RequestProductSearchDto;
import com.wl2c.elswhereproductservice.domain.product.model.dto.response.ResponseProductComparisonTargetDto;
import com.wl2c.elswhereproductservice.domain.product.model.dto.response.ResponseSingleProductDto;
@@ -18,9 +23,12 @@
import com.wl2c.elswhereproductservice.domain.product.repository.TickerSymbolRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
+import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.data.domain.*;
import org.springframework.stereotype.Service;
+import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
@@ -29,6 +37,9 @@
@Slf4j
public class ProductService {
+ private final AnalysisServiceClient analysisServiceClient;
+ private final CircuitBreakerFactory circuitBreakerFactory;
+
private final ProductRepository productRepository;
private final TickerSymbolRepository tickerSymbolRepository;
private final ProductSearchRepository productSearchRepository;
@@ -37,7 +48,7 @@ public class ProductService {
private final LikeService likeService;
private final DailyHotProductService dailyHotProductService;
- public Page listByOnSale(String type, Pageable pageable) {
+ public Page listByOnSale(String type, Pageable pageable) {
Sort sort = switch (type) {
case "latest" -> Sort.by(Sort.Order.desc("subscriptionStartDate"), Sort.Order.desc("lastModifiedAt"));
case "knock-in" -> Sort.by(Sort.Order.asc("knockIn").nullsLast(), Sort.Order.desc("lastModifiedAt"));
@@ -53,7 +64,28 @@ public Page listByOnSale(String type, Pageable pageable) {
Pageable sortedPageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), sort);
Page products = productRepository.listByOnSale(type, sortedPageable);
- return products.map(SummarizedProductDto::new);
+ List stepDownProductIds = products.getContent().stream()
+ .filter(product -> product.getType() == ProductType.STEP_DOWN) // STEP_DOWN 타입 필터링
+ .map(Product::getId)
+ .toList();
+ List responseStepDownAIResultDtos = listStepDownAIResult(stepDownProductIds);
+
+ // AI 결과 리스트에서 productId를 key로 하는 Map으로 변환
+ Map productSafetyScoreMap = responseStepDownAIResultDtos.stream()
+ .collect(Collectors.toMap(
+ ResponseAIResultDto::getProductId,
+ ResponseAIResultDto::getSafetyScore,
+ (existing, replacement) -> existing // 중복된 경우 기존 값 사용
+ ));
+
+ List summarizedProducts = products.getContent().stream()
+ .map(product -> {
+ BigDecimal safetyScore = productSafetyScoreMap.getOrDefault(product.getId(), null);
+ return new SummarizedOnSaleProductDto(product, safetyScore);
+ })
+ .toList();
+
+ return new PageImpl<>(summarizedProducts, pageable, products.getTotalElements());
}
public Page listByEndSale(String type, Pageable pageable) {
@@ -118,17 +150,41 @@ public ResponseSingleProductDto findOne(Long productId, Long userId) {
public Map> findComparisonTargets(Long id) {
Map> result = new HashMap<>();
+ // 타겟 상품
Product product = productRepository.isItProductOnSale(id).orElseThrow(NotOnSaleProductException::new);
List tickerSymbolEntityList = tickerSymbolRepository.findTickerSymbolList(id);
List tickerSymbolList = tickerSymbolEntityList.stream()
.map(TickerSymbol::getTickerSymbol)
.toList();
- List target = new ArrayList<>();
- target.add(new ResponseProductComparisonTargetDto(product));
+ // 비교 상품
List productComparisonResults = productRepository.findComparisonResults(id, product.getEquityCount(), tickerSymbolList);
+
+ // 타겟 및 비교 상품들 중 STEP_DOWN 타입 필터링
+ List stepDownProductIds = new ArrayList<>(productComparisonResults.stream()
+ .filter(productComparisonResult -> productComparisonResult.getType() == ProductType.STEP_DOWN)
+ .map(Product::getId)
+ .toList());
+ if (product.getType() == ProductType.STEP_DOWN)
+ stepDownProductIds.add(product.getId());
+ List responseStepDownAIResultDtos = listStepDownAIResult(stepDownProductIds);
+
+ // AI 결과 리스트에서 productId를 key로 하는 Map으로 변환
+ Map productSafetyScoreMap = responseStepDownAIResultDtos.stream()
+ .collect(Collectors.toMap(
+ ResponseAIResultDto::getProductId,
+ ResponseAIResultDto::getSafetyScore,
+ (existing, replacement) -> existing // 중복된 경우 기존 값 사용
+ ));
+
+ List target = new ArrayList<>();
+ target.add(new ResponseProductComparisonTargetDto(product, productSafetyScoreMap.getOrDefault(product.getId(), null)));
+
List comparisonResults = productComparisonResults.stream()
- .map(ResponseProductComparisonTargetDto::new)
+ .map(productComparisonResult -> new ResponseProductComparisonTargetDto(
+ productComparisonResult,
+ productSafetyScoreMap.getOrDefault(productComparisonResult.getId(), null)
+ ))
.toList();
result.put("target", target);
@@ -187,4 +243,10 @@ public List getDailyTop5Products() {
return summarizedProductDtos;
}
+
+ private List listStepDownAIResult(List stepDownProductIds) {
+ CircuitBreaker circuitBreaker = circuitBreakerFactory.create("aiResultCircuitBreaker");
+ return circuitBreaker.run(() -> analysisServiceClient.getAIResultList(new RequestProductIdListDto(stepDownProductIds)),
+ throwable -> new ArrayList<>());
+ }
}