Repository: auction-guys/aucjangpan
Pull request #24: 시세조회 기능 구현
Author: login: k1130k
Created: 2025-04-10T11:44:25Z | Merged: 2025-04-18T05:07:21Z

* OpenAIAPI를 통한 시세조회
* 당일 시세는 레디스 캐싱, 최근 3개월시세는 DB에 저장
* @Cacheable을 통해 레디스 캐시 만료후 재호출
* 네이버 오픈API를 통해 실제 시세 확인 -> 네이버 쇼핑에 올라와있는 실제 시세를 가져온 뒤 GPT를 통해 가공을 해 최근 3개월 및 오늘 시세 반영


Diff:
diff --git a/build.gradle b/build.gradle
index 5f8810cf..3717c131 100644
--- a/build.gradle
+++ b/build.gradle
@@ -86,4 +86,4 @@ dependencies {
 
 tasks.named('test') {
     useJUnitPlatform()
-}
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 84fe0d8b..082f4ff1 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-rootProject.name = 'auction'
+rootProject.name = 'aucjangpan'
diff --git a/src/main/java/com/fifteen/auction/AuctionApplication.java b/src/main/java/com/fifteen/auction/AuctionApplication.java
index 69ca06e7..5d257f36 100644
--- a/src/main/java/com/fifteen/auction/AuctionApplication.java
+++ b/src/main/java/com/fifteen/auction/AuctionApplication.java
@@ -3,9 +3,11 @@
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.scheduling.annotation.EnableScheduling;
 
 @SpringBootApplication
 @EnableFeignClients
+@EnableScheduling
 public class AuctionApplication {
 
     public static void main(String[] args) {
diff --git a/src/main/java/com/fifteen/auction/domain/auction/dto/response/AuctionDetail.java b/src/main/java/com/fifteen/auction/domain/auction/dto/response/AuctionDetail.java
index 40c5c485..3b9becb4 100644
--- a/src/main/java/com/fifteen/auction/domain/auction/dto/response/AuctionDetail.java
+++ b/src/main/java/com/fifteen/auction/domain/auction/dto/response/AuctionDetail.java
@@ -4,6 +4,7 @@
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fifteen.auction.domain.auction.entity.Auction;
+import com.fifteen.auction.domain.product.dto.response.MarketPriceFullResponse;
 import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
@@ -29,12 +30,11 @@ public class AuctionDetail {
     @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm", timezone = "Asia/Seoul")
     private LocalDateTime expiresAt;
 
-    // 조회수 필드 추가
     private Integer views;
 
+    private MarketPriceFullResponse marketPrice;
 
-    //조회수 views 추가
-    public static AuctionDetail fromAuction(Auction auction) {
+    public static AuctionDetail fromAuction(Auction auction, MarketPriceFullResponse marketPrice) {
         return new AuctionDetail(
                 auction.getProduct().getName(),
                 auction.getAuctionSeq(),
@@ -43,7 +43,8 @@ public static AuctionDetail fromAuction(Auction auction) {
                 auction.isBuyNowSet(),
                 auction.isAutoExtensible(),
                 auction.getExpiresAt(),
-                auction.getViews()
+                auction.getViews(),
+                marketPrice
         );
     }
 }
diff --git a/src/main/java/com/fifteen/auction/domain/auction/service/AuctionService.java b/src/main/java/com/fifteen/auction/domain/auction/service/AuctionService.java
index cf9ee041..d7ac370a 100644
--- a/src/main/java/com/fifteen/auction/domain/auction/service/AuctionService.java
+++ b/src/main/java/com/fifteen/auction/domain/auction/service/AuctionService.java
@@ -9,8 +9,10 @@
 import com.fifteen.auction.domain.auction.entity.AuctionStatus;
 import com.fifteen.auction.domain.auction.repository.auction.AuctionRepository;
 import com.fifteen.auction.domain.auction.util.AuctionSeqGenerator;
+import com.fifteen.auction.domain.product.dto.response.MarketPriceFullResponse;
 import com.fifteen.auction.domain.product.entity.Product;
 import com.fifteen.auction.domain.product.repository.ProductRepository;
+import com.fifteen.auction.domain.product.service.MarketPriceService;
 import com.fifteen.auction.global.dto.PageCond;
 import com.fifteen.auction.global.dto.error.ErrorCode;
 import com.fifteen.auction.global.dto.exception.ClientException;
@@ -31,6 +33,7 @@ public class AuctionService {
     private final AuctionRepository auctionRepository;
     private final ProductRepository productRepository;
     private final AuctionSeqGenerator auctionSeqGenerator;
+    private final MarketPriceService marketPriceService;
     private final AuctionCacheService auctionCacheService;
     private final ApplicationEventPublisher applicationEventPublisher;
 
@@ -51,9 +54,7 @@ public String create(AuctionCreateRequest req, Long userId) {
                 req.getStartPrice(), req.getBuyNowPrice(), req.getBidUnit(),
                 req.getIsBuyNowSet(), req.getIsAutoExtensible(), req.getExpiresAt());
 
-        Auction savedAuction = auctionRepository.save(auction);
-
-        return savedAuction.getAuctionSeq();
+        return auctionRepository.save(auction).getAuctionSeq();
     }
 
 
@@ -126,17 +127,18 @@ public AuctionDetail findOne(String auctionSeq) {
         Auction findAuction = auctionRepository
                 .findOpenOneByAuctionSeq(auctionSeq)
                 .orElseThrow(() -> new ClientException(ErrorCode.AUCTION_NOT_FOUND));
-        return AuctionDetail.fromAuction(findAuction);
+        MarketPriceFullResponse marketPrice = marketPriceService.findMarketPriceFullResponse(findAuction.getProduct().getId());
+        return AuctionDetail.fromAuction(findAuction, marketPrice);
     }
 
     @Transactional
     public AuctionDetail getAuctionDetailAndIncreaseView(Long auctionId) {
-        Auction auction = auctionRepository.findById(auctionId)
+        Auction findAuction = auctionRepository.findById(auctionId)
                 .orElseThrow(() -> new ClientException(ErrorCode.AUCTION_NOT_FOUND));
 
-        auction.increaseViews();  // views += 1
-
-        return AuctionDetail.fromAuction(auction);
+        findAuction.increaseViews();  // views += 1
+        MarketPriceFullResponse marketPrice = marketPriceService.findMarketPriceFullResponse(findAuction.getProduct().getId());
+        return AuctionDetail.fromAuction(findAuction, marketPrice);
     }
 
 }
diff --git a/src/main/java/com/fifteen/auction/domain/product/dto/response/GPTPricePredictionResponse.java b/src/main/java/com/fifteen/auction/domain/product/dto/response/GPTPricePredictionResponse.java
new file mode 100644
index 00000000..68308560
--- /dev/null
+++ b/src/main/java/com/fifteen/auction/domain/product/dto/response/GPTPricePredictionResponse.java
@@ -0,0 +1,14 @@
+package com.fifteen.auction.domain.product.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public class GPTPricePredictionResponse {
+    private String date;
+    private Long min;
+    private Long max;
+}
diff --git a/src/main/java/com/fifteen/auction/domain/product/dto/response/MarketPriceFullResponse.java b/src/main/java/com/fifteen/auction/domain/product/dto/response/MarketPriceFullResponse.java
new file mode 100644
index 00000000..66e79bac
--- /dev/null
+++ b/src/main/java/com/fifteen/auction/domain/product/dto/response/MarketPriceFullResponse.java
@@ -0,0 +1,17 @@
+package com.fifteen.auction.domain.product.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Getter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class MarketPriceFullResponse {
+    private MarketPriceResponse todayPrice; // 오늘 시세 (Redis 캐시)
+    private List<MarketPriceResponse> historicalPrices; // 최근 3개월 시세 (DB)
+}
diff --git a/src/main/java/com/fifteen/auction/domain/product/dto/response/MarketPriceResponse.java b/src/main/java/com/fifteen/auction/domain/product/dto/response/MarketPriceResponse.java
new file mode 100644
index 00000000..ce167380
--- /dev/null
+++ b/src/main/java/com/fifteen/auction/domain/product/dto/response/MarketPriceResponse.java
@@ -0,0 +1,31 @@
+package com.fifteen.auction.domain.product.dto.response;
+
+import com.fifteen.auction.domain.product.entity.MarketPrice;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+@Getter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class MarketPriceResponse {
+
+    private Long productId;
+    private Long minMarketPrice;
+    private Long maxMarketPrice;
+    private LocalDate priceDate;
+
+    public static MarketPriceResponse fromEntity(MarketPrice marketPrice) {
+        return MarketPriceResponse.builder()
+                .productId(marketPrice.getProduct().getId())
+                .minMarketPrice(marketPrice.getMinMarketPrice())
+                .maxMarketPrice(marketPrice.getMaxMarketPrice())
+                .priceDate(marketPrice.getPriceDate())
+                .build();
+    }
+}
diff --git a/src/main/java/com/fifteen/auction/domain/product/dto/response/NaverShoppingItemDto.java b/src/main/java/com/fifteen/auction/domain/product/dto/response/NaverShoppingItemDto.java
new file mode 100644
index 00000000..bf012c35
--- /dev/null
+++ b/src/main/java/com/fifteen/auction/domain/product/dto/response/NaverShoppingItemDto.java
@@ -0,0 +1,16 @@
+package com.fifteen.auction.domain.product.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class NaverShoppingItemDto {
+    private String title;       // 상품명
+    private Long lprice;        // 최저가
+    private String mallName;    // 쇼핑몰 이름
+}
diff --git a/src/main/java/com/fifteen/auction/domain/product/entity/MarketPrice.java b/src/main/java/com/fifteen/auction/domain/product/entity/MarketPrice.java
index d43b14f5..6b2fe50d 100644
--- a/src/main/java/com/fifteen/auction/domain/product/entity/MarketPrice.java
+++ b/src/main/java/com/fifteen/auction/domain/product/entity/MarketPrice.java
@@ -2,23 +2,27 @@
 
 import com.fifteen.auction.global.entity.BaseEntity;
 import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 
-import java.time.LocalDateTime;
+import java.time.LocalDate;
 
 @Getter
 @Entity
 @NoArgsConstructor
+@Builder
+@AllArgsConstructor
 public class MarketPrice extends BaseEntity {
+
     @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
     private Long id;
 
-    @OneToOne
+    @ManyToOne(fetch = FetchType.LAZY)
     @JoinColumn(name = "product_id")
     private Product product;
+    private LocalDate priceDate;
     private Long minMarketPrice;
     private Long maxMarketPrice;
-    private LocalDateTime createdAt;
-    private LocalDateTime updatedAt;
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/fifteen/auction/domain/product/entity/Product.java b/src/main/java/com/fifteen/auction/domain/product/entity/Product.java
index 50128b93..b0ba6759 100644
--- a/src/main/java/com/fifteen/auction/domain/product/entity/Product.java
+++ b/src/main/java/com/fifteen/auction/domain/product/entity/Product.java
@@ -4,12 +4,12 @@
 import com.fifteen.auction.global.dto.error.ErrorCode;
 import com.fifteen.auction.global.dto.exception.ClientException;
 import com.fifteen.auction.global.entity.BaseEntity;
-import com.fifteen.auction.domain.product.entity.ProductCategory;
 import jakarta.persistence.*;
 import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 
+import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.List;
@@ -74,4 +74,4 @@ public ProductImage getThumbnailImage() {
                 .findFirst()
                 .orElseThrow(() -> new ClientException(ErrorCode.PRODUCT_IMAGE_NOT_FOUND));
     }
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/fifteen/auction/domain/product/entity/ProductCategory.java b/src/main/java/com/fifteen/auction/domain/product/entity/ProductCategory.java
index e2a52d3e..70b1b28c 100644
--- a/src/main/java/com/fifteen/auction/domain/product/entity/ProductCategory.java
+++ b/src/main/java/com/fifteen/auction/domain/product/entity/ProductCategory.java
@@ -1,5 +1,6 @@
 package com.fifteen.auction.domain.product.entity;
 
+
 import com.fifteen.auction.global.entity.BaseEntity;
 import jakarta.persistence.*;
 import lombok.AccessLevel;
@@ -23,6 +24,7 @@ public class ProductCategory extends BaseEntity {
     @Column(nullable = false, unique = true, length = 50)
     private String name;
 
+    /** 상위 카테고리 여부 확장성 고려 */
     @ManyToOne(fetch = FetchType.LAZY)
     @JoinColumn(name = "parent_id")
     private ProductCategory parent;
diff --git a/src/main/java/com/fifteen/auction/domain/product/repository/MarketPriceRepository.java b/src/main/java/com/fifteen/auction/domain/product/repository/MarketPriceRepository.java
index 97c29253..94f585ac 100644
--- a/src/main/java/com/fifteen/auction/domain/product/repository/MarketPriceRepository.java
+++ b/src/main/java/com/fifteen/auction/domain/product/repository/MarketPriceRepository.java
@@ -3,5 +3,15 @@
 import com.fifteen.auction.domain.product.entity.MarketPrice;
 import org.springframework.data.jpa.repository.JpaRepository;
 
+import java.time.LocalDate;
+import java.util.List;
+
 public interface MarketPriceRepository extends JpaRepository<MarketPrice, Long> {
+
+    // 상품의 특정 날짜 시세가 이미 존재하는지 확인
+    boolean existsByProductIdAndPriceDate(Long productId, LocalDate marketDate);
+
+    // 상품의 최근 3개월 시세 전체 조회
+    List<MarketPrice> findAllByProductIdOrderByPriceDateAsc(Long productId);
 }
+
diff --git a/src/main/java/com/fifteen/auction/domain/product/repository/ProductRepository.java b/src/main/java/com/fifteen/auction/domain/product/repository/ProductRepository.java
index bb8ddc2d..de4c1bd2 100644
--- a/src/main/java/com/fifteen/auction/domain/product/repository/ProductRepository.java
+++ b/src/main/java/com/fifteen/auction/domain/product/repository/ProductRepository.java
@@ -5,6 +5,9 @@
 import org.springframework.data.jpa.repository.Query;
 import org.springframework.data.repository.query.Param;
 
+import java.time.LocalDate;
+import java.util.Collection;
+import java.util.List;
 import java.util.Optional;
 
 public interface ProductRepository extends JpaRepository<Product, Long> {
@@ -17,8 +20,10 @@ public interface ProductRepository extends JpaRepository<Product, Long> {
     // (추후 필요 시 보류)
     // Page<Product> findAllBySellerIdAndDeletedAtIsNull(Long sellerId, Pageable pageable);
     // List<Product> findByCategoryAndDeletedAtIsNull(ProductCategory category);
-
+//
 //    Page<Product> findAllBySellerIdAndDeletedFalse(Long sellerId, Pageable pageable);
+//
+    List<Product> findByIdNotIn(Collection<Long> ids);
 //
 //    List<Product> findByCategoryAndDeletedFalse(ProductCategory category);
 //
@@ -26,7 +31,7 @@ public interface ProductRepository extends JpaRepository<Product, Long> {
 //
 //    Page<Product> findAllByDeletedFalse(Pageable pageable);
 
-    @Query("select p from Product p join fetch p.seller where p.id = :productId and p.deletedAt != null ")
+    @Query("select p from Product p join fetch p.seller where p.id = :productId and p.deletedAt is null ")
     Optional<Product> findByIdWithSeller(@Param("productId") Long id);
 
 }
\ No newline at end of file
diff --git a/src/main/java/com/fifteen/auction/domain/product/service/MarketPriceService.java b/src/main/java/com/fifteen/auction/domain/product/service/MarketPriceService.java
new file mode 100644
index 00000000..15c65f52
--- /dev/null
+++ b/src/main/java/com/fifteen/auction/domain/product/service/MarketPriceService.java
@@ -0,0 +1,99 @@
+package com.fifteen.auction.domain.product.service;
+
+import com.fifteen.auction.domain.product.dto.response.GPTPricePredictionResponse;
+import com.fifteen.auction.domain.product.dto.response.MarketPriceFullResponse;
+import com.fifteen.auction.domain.product.dto.response.MarketPriceResponse;
+import com.fifteen.auction.domain.product.entity.MarketPrice;
+import com.fifteen.auction.domain.product.entity.Product;
+import com.fifteen.auction.domain.product.repository.MarketPriceRepository;
+import com.fifteen.auction.domain.product.repository.ProductRepository;
+import com.fifteen.auction.global.client.OpenAIClient;
+import com.fifteen.auction.global.dto.error.ErrorCode;
+import com.fifteen.auction.global.dto.exception.ServerException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDate;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class MarketPriceService {
+
+    private final MarketPriceRepository marketPriceRepository;
+    private final ProductRepository productRepository;
+    private final OpenAIClient openAIClient;
+
+    @Cacheable(value = "marketPrice", key = "#productId")
+    @Transactional
+    public MarketPriceFullResponse findMarketPriceFullResponse(Long productId) {
+        log.info("Redis 캐시 만료! 네이버 API + GPT 호출 & DB 저장! 상품 ID: {}", productId);
+
+        Product product = productRepository.findById(productId)
+                .orElseThrow(() -> new ServerException(ErrorCode.PRODUCT_NOT_FOUND));
+
+        // 네이버 API + GPT 호출을 통해 오늘 포함 최근 3개월 시세 예측
+        List<GPTPricePredictionResponse> predictedPrices = openAIClient.callGptForHistoricalPrices(
+                product.getName(),
+                product.getDescription()
+        );
+
+        if (predictedPrices.isEmpty()) {
+            throw new ServerException(ErrorCode.MARKET_PRICE_NOT_FOUND);
+        }
+
+        Set<LocalDate> savedDates = new HashSet<>();
+        MarketPrice todayPrice = null;
+
+        for (int i = 0; i < predictedPrices.size(); i++) {
+            GPTPricePredictionResponse dto = predictedPrices.get(i);
+            LocalDate priceDate = LocalDate.parse(dto.getDate());
+
+            // 중복 날짜 필터링
+            if (!savedDates.add(priceDate)) continue;
+
+            boolean isToday = (i == predictedPrices.size() - 1);
+            boolean alreadySaved = marketPriceRepository.existsByProductIdAndPriceDate(productId, priceDate);
+
+            // 이미 저장된 과거 데이터는 스킵
+            if (!isToday && alreadySaved) continue;
+
+            MarketPrice price = MarketPrice.builder()
+                    .product(product)
+                    .priceDate(priceDate)
+                    .minMarketPrice(dto.getMin())
+                    .maxMarketPrice(dto.getMax())
+                    .build();
+
+            if (!isToday) {
+                // 과거 데이터만 DB 저장
+                marketPriceRepository.save(price);
+            } else {
+                // 오늘 데이터는 변수에만 저장하고, DB 저장 안 함
+                todayPrice = price;
+            }
+        }
+
+        if (todayPrice == null) {
+            throw new ServerException(ErrorCode.MARKET_PRICE_NOT_FOUND);
+        }
+
+        // DB에 저장된 최근 3개월 시세 조회 및 DTO 반환
+        List<MarketPriceResponse> historicalPrices = marketPriceRepository
+                .findAllByProductIdOrderByPriceDateAsc(productId)
+                .stream()
+                .map(MarketPriceResponse::fromEntity)
+                .toList();
+
+        return MarketPriceFullResponse.builder()
+                .todayPrice(MarketPriceResponse.fromEntity(todayPrice))
+                .historicalPrices(historicalPrices)
+                .build();
+    }
+}
diff --git a/src/main/java/com/fifteen/auction/domain/product/service/ProductService.java b/src/main/java/com/fifteen/auction/domain/product/service/ProductService.java
index 52f66f65..628cd864 100644
--- a/src/main/java/com/fifteen/auction/domain/product/service/ProductService.java
+++ b/src/main/java/com/fifteen/auction/domain/product/service/ProductService.java
@@ -31,6 +31,7 @@ public class ProductService {
     private final ProductImageService productImageUploadService;
     private final ProductCategoryRepository categoryRepository;
     private final UserRepository userRepository;
+    private final MarketPriceService marketPriceService;
 
     public Long createProduct(Long sellerId, ProductCreateRequest request) {
         User seller = userRepository.findById(sellerId)
@@ -47,7 +48,12 @@ public Long createProduct(Long sellerId, ProductCreateRequest request) {
             product.addImage(image);
         }
 
-        return productRepository.save(product).getId();
+        Product saved = productRepository.save(product);
+
+        // GPT 시세 예측 후 저장 (Redis + DB)
+        marketPriceService.findMarketPriceFullResponse(saved.getId());
+
+        return saved.getId();
     }
 
     public void updateProduct(Long sellerId, Long productId, ProductUpdateRequest request) {
diff --git a/src/main/java/com/fifteen/auction/global/client/NaverSearchClient.java b/src/main/java/com/fifteen/auction/global/client/NaverSearchClient.java
new file mode 100644
index 00000000..5b490132
--- /dev/null
+++ b/src/main/java/com/fifteen/auction/global/client/NaverSearchClient.java
@@ -0,0 +1,93 @@
+package com.fifteen.auction.global.client;
+
+import com.fifteen.auction.domain.product.dto.response.NaverShoppingItemDto;
+import com.fifteen.auction.global.dto.error.ErrorCode;
+import com.fifteen.auction.global.dto.exception.ServerException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class NaverSearchClient {
+
+    @Value("${naver.api.id}")
+    private String clientId;
+
+    @Value("${naver.api.secret}")
+    private String clientSecret;
+
+    @Value("${naver.api.url}")
+    private String shoppingUrl;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+     //네이버 쇼핑 검색 API를 호출하여 상품 검색 결과를 반환
+    public List<NaverShoppingItemDto> findShoppingItems(String query) {
+        log.info("네이버 쇼핑 API 호출 시작: 검색어 '{}'", query);
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("X-Naver-Client-Id", clientId);
+        headers.set("X-Naver-Client-Secret", clientSecret);
+        headers.setContentType(MediaType.APPLICATION_JSON);
+
+        URI uri = UriComponentsBuilder
+                .fromUriString(shoppingUrl)
+                .queryParam("query", query)
+                .queryParam("display", 50)
+                .queryParam("sort", "sim")
+                .build()
+                .encode(StandardCharsets.UTF_8)
+                .toUri();
+
+        RequestEntity<Void> requestEntity = new RequestEntity<>(
+                headers, HttpMethod.GET, uri
+        );
+
+        try {
+            ResponseEntity<Map> responseEntity = restTemplate.exchange(
+                    requestEntity,
+                    Map.class
+            );
+
+            Map<String, Object> responseBody = responseEntity.getBody();
+            if (responseBody == null || !responseBody.containsKey("items")) {
+                return new ArrayList<>();
+            }
+
+            List<Map<String, Object>> items = (List<Map<String, Object>>) responseBody.get("items");
+            log.info("네이버 쇼핑 API 검색 성공: 검색어 '{}', 결과 {}개 항목", query, items.size());
+            return items.stream()
+                    .map(item -> NaverShoppingItemDto.builder()
+                            .title((String) item.get("title"))
+                            .lprice(parseLongOrDefault((String) item.get("lprice"), 0L))
+                            .mallName((String) item.get("mallName"))
+                            .build())
+                    .toList();
+        } catch (Exception e) {
+            log.error("네이버 쇼핑 API 호출 실패: 검색어 '{}', 오류: {}", query, e.getMessage(), e);
+            throw new ServerException(ErrorCode.EXTERNAL_API_ERROR);
+        }
+    }
+
+    private Long parseLongOrDefault(String value, Long defaultValue) {
+        if (value == null || value.isEmpty()) {
+            return defaultValue;
+        }
+        try {
+            return Long.parseLong(value);
+        } catch (NumberFormatException e) {
+            return defaultValue;
+        }
+    }
+}
diff --git a/src/main/java/com/fifteen/auction/global/client/OpenAIClient.java b/src/main/java/com/fifteen/auction/global/client/OpenAIClient.java
new file mode 100644
index 00000000..fbe511ab
--- /dev/null
+++ b/src/main/java/com/fifteen/auction/global/client/OpenAIClient.java
@@ -0,0 +1,162 @@
+package com.fifteen.auction.global.client;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fifteen.auction.domain.product.dto.response.GPTPricePredictionResponse;
+import com.fifteen.auction.domain.product.dto.response.NaverShoppingItemDto;
+import com.fifteen.auction.global.dto.error.ErrorCode;
+import com.fifteen.auction.global.dto.exception.ServerException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class OpenAIClient {
+
+    @Value("${openai.api.url}")
+    private String apiUrl;
+
+    @Value("${openai.api.key}")
+    private String apiKey;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+    private final NaverSearchClient naverSearchClient;
+
+    public List<GPTPricePredictionResponse> callGptForHistoricalPrices(String title, String description) {
+        log.info("상품 '{}' 시세 예측 시작 - 네이버 쇼핑 API 호출", title);
+        List<NaverShoppingItemDto> shoppingItems = naverSearchClient.findShoppingItems(title);
+
+        String marketSummary = null;
+
+        if (!shoppingItems.isEmpty()) {
+            log.info("상품 '{}' 네이버 검색 결과 {}개 항목 - GPT 예측 시작", title, shoppingItems.size());
+
+            long minPrice = Long.MAX_VALUE;
+            long maxPrice = 0L;
+            long totalPrice = 0L;
+            int validItemCount = 0;
+
+            StringBuilder summary = new StringBuilder();
+            summary.append("현재 최신 시장 데이터 (네이버 쇼핑):\n");
+
+            for (NaverShoppingItemDto item : shoppingItems) {
+                if (item.getLprice() != null && item.getLprice() > 0) {
+                    minPrice = Math.min(minPrice, item.getLprice());
+                    maxPrice = Math.max(maxPrice, item.getLprice());
+                    totalPrice += item.getLprice();
+                    validItemCount++;
+
+                    summary.append(String.format("- %s: %,d원 (판매처: %s)\n",
+                            item.getTitle().replaceAll("<[^>]*>", ""),
+                            item.getLprice(),
+                            item.getMallName()));
+
+                    if (validItemCount >= 10) break;
+                }
+            }
+
+            if (validItemCount > 0) {
+                summary.append(String.format("\n현재 최저가: %,d원\n", minPrice));
+                summary.append(String.format("현재 최고가: %,d원\n", maxPrice));
+                summary.append(String.format("현재 평균가: %,d원\n", totalPrice / validItemCount));
+                marketSummary = summary.toString();
+            }
+        }
+
+        if (shoppingItems.isEmpty() || marketSummary == null) {
+            log.info("상품 '{}' 네이버 검색 결과 없음 또는 유효하지 않음 - ChatGPT를 사용한 예측으로 전환", title);
+        }
+
+        String prompt = buildPrompt(title, description, marketSummary);
+        return callGptWithPrompt(prompt);
+    }
+
+    private String buildPrompt(String title, String description, String marketSummary) {
+        LocalDate today = LocalDate.now();
+        List<LocalDate> dates = IntStream.rangeClosed(1, 3)
+                .mapToObj(i -> today.minusMonths(i).withDayOfMonth(1))
+                .collect(Collectors.toList());
+        Collections.reverse(dates);
+        dates.add(today); // 오늘 포함
+
+        String exampleJson = buildExampleJsonFormat(dates);
+
+        return String.format("""
+            다음 중고 상품의 최근 3개월 (1일 기준)과 오늘(%s)의 예상 거래 가격 범위를 알려줘.
+            형식은 다음과 같이 JSON 배열로 정확하게 반환해줘:
+
+            ※ 가격 숫자는 쉼표(,) 없이 정수로만 반환해주고 
+            모든 key는 반드시 따옴표로 감싸줘.
+             value는 숫자로 주고 절대 다른 텍스트 포함하지마
+
+            %s
+
+            %s
+            제품명: %s
+            설명: %s
+            """,
+                today.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),
+                exampleJson,
+                marketSummary != null ? marketSummary + "\n" : "",
+                title,
+                description
+        );
+    }
+
+    private String buildExampleJsonFormat(List<LocalDate> dates) {
+        StringBuilder exampleBuilder = new StringBuilder("[\n");
+        for (LocalDate date : dates) {
+            exampleBuilder.append(String.format("  { \"date\": \"%s\", \"min\": 최저가, \"max\": 최고가 },\n",
+                    date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))));
+        }
+        exampleBuilder.setLength(exampleBuilder.length() - 2); // 마지막 콤마 제거
+        exampleBuilder.append("\n]");
+        return exampleBuilder.toString();
+    }
+
+    private List<GPTPricePredictionResponse> callGptWithPrompt(String prompt) {
+        Map<String, Object> request = new HashMap<>();
+        request.put("model", "gpt-3.5-turbo");
+        request.put("messages", List.of(Map.of("role", "user", "content", prompt)));
+        request.put("temperature", 0.5);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        headers.setBearerAuth(apiKey);
+
+        HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);
+
+        try {
+            ResponseEntity<String> response = restTemplate.exchange(
+                    apiUrl,
+                    HttpMethod.POST,
+                    entity,
+                    String.class
+            );
+
+            log.info("GPT 응답 원문: {}", response.getBody());
+
+            ObjectMapper objectMapper = new ObjectMapper();
+            JsonNode root = objectMapper.readTree(response.getBody());
+            String content = root.path("choices").get(0).path("message").path("content").asText();
+
+            return objectMapper.readValue(
+                    content,
+                    objectMapper.getTypeFactory().constructCollectionType(List.class, GPTPricePredictionResponse.class)
+            );
+        } catch (Exception e) {
+            log.error("GPT API 호출 실패", e);
+            throw new ServerException(ErrorCode.EXTERNAL_API_ERROR);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/fifteen/auction/global/config/RedisCacheConfig.java b/src/main/java/com/fifteen/auction/global/config/RedisCacheConfig.java
new file mode 100644
index 00000000..e651eff4
--- /dev/null
+++ b/src/main/java/com/fifteen/auction/global/config/RedisCacheConfig.java
@@ -0,0 +1,45 @@
+package com.fifteen.auction.global.config;
+
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+
+import java.time.Duration;
+
+@Configuration
+@EnableCaching // 캐시 기능 활성화
+public class RedisCacheConfig {
+
+    @Bean
+    public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
+        ObjectMapper objectMapper = new ObjectMapper();
+        objectMapper.registerModule(new JavaTimeModule());
+        objectMapper.activateDefaultTyping(
+                LaissezFaireSubTypeValidator.instance,
+                ObjectMapper.DefaultTyping.NON_FINAL,
+                JsonTypeInfo.As.PROPERTY
+        );
+
+        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
+                .entryTtl(Duration.ofHours(24)) // TTL 24시간
+                .disableCachingNullValues()
+                .serializeValuesWith(RedisSerializationContext
+                        .SerializationPair
+                        .fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper))
+                );
+
+        return RedisCacheManager.builder(connectionFactory)
+                .cacheDefaults(config)
+                .build();
+    }
+}
diff --git a/src/main/java/com/fifteen/auction/global/dto/error/ErrorCode.java b/src/main/java/com/fifteen/auction/global/dto/error/ErrorCode.java
index b04c6cd6..da0103e8 100644
--- a/src/main/java/com/fifteen/auction/global/dto/error/ErrorCode.java
+++ b/src/main/java/com/fifteen/auction/global/dto/error/ErrorCode.java
@@ -48,7 +48,7 @@ public enum ErrorCode {
     PASSWORDS_DO_NOT_MATCH(HttpStatus.BAD_REQUEST, "USER-12", "비밀번호와 비밀번호 확인이 일치하지 않습니다."),
     PASSWORD_ALREADY_SET(HttpStatus.BAD_REQUEST, "USER-13", "이미 비밀번호가 설정된 사용자입니다."),
 
-    // Auction Exceptions 에러 코드
+    // Auction Exceptions
     AUCTION_NOT_FOUND(HttpStatus.NOT_FOUND, "AUCTION-1", "경매가 존재하지 않습니다."),
     NOT_OWNING_PRODUCT(HttpStatus.BAD_REQUEST, "AUCTION-2", "자신의 상품만 경매에 등록할 수 있습니다."),
     INVALID_BID_REQUEST(HttpStatus.BAD_REQUEST, "AUCTION-3", "입찰할 수 없는 경매입니다."),
@@ -57,15 +57,21 @@ public enum ErrorCode {
     AUCTION_ACCESS_DENIED(HttpStatus.FORBIDDEN, "AUCTION-6", "해당 경매에 접근할 수 없습니다."),
     AUCTION_ALREADY_OPEN(HttpStatus.BAD_REQUEST, "AUCTION-7", "이미 공개된 경매입니다."),
 
-    // Product Custom Exception 에러 코드
+    // Product Custom Exception
     PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, "PRODUCT-1", "존재하지 않는 상품입니다."),
     PRODUCT_CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "PRODUCT-2", "존재하지 않는 카테고리입니다."),
     PRODUCT_IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "PRODUCT-3", "대표 이미지가 존재하지 않습니다."),
     UNAUTHORIZED(HttpStatus.FORBIDDEN, "COMMON-1", "접근 권한이 없습니다."),
 
+    // Market Price Exceptions
+    MARKET_PRICE_NOT_FOUND(HttpStatus.NOT_FOUND, "MARKETPRICE-1", "시세 정보가 존재하지 않습니다."),
+
+    // Naver API Exceptions
+    EXTERNAL_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "API-1", "네이버 오픈 API 호출 중 오류가 발생했습니다."),
+
     // FAVORITE 에러 코드
-    DUPLICATE_FAVORITE(HttpStatus.CONFLICT, "FAVORITE_001", "이미 찜한 경매입니다."),
-    FAVORITE_NOT_FOUND(HttpStatus.NOT_FOUND, "FAVORITE_002", "찜 내역이 존재하지 않습니다."),
+    DUPLICATE_FAVORITE(HttpStatus.CONFLICT, "FAVORITE-1", "이미 찜한 경매입니다."),
+    FAVORITE_NOT_FOUND(HttpStatus.NOT_FOUND, "FAVORITE-2", "찜 내역이 존재하지 않습니다."),
 
     // Image Exception 에러 코드
     UPLOAD_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "IMAGE-1", "이미지 업로드에 실패했습니다."),
@@ -90,4 +96,4 @@ public enum ErrorCode {
     private final HttpStatus status;
     private final String code;
     private final String message;
-}
+}
\ No newline at end of file
diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml
index 6dea7e01..eac9d98b 100644
--- a/src/main/resources/application-prod.yml
+++ b/src/main/resources/application-prod.yml
@@ -63,4 +63,14 @@ management:
 metrics:
   export:
     prometheus:
-      enabled: true
\ No newline at end of file
+      enabled: true
+
+openai:
+  api:
+    key: ${OPENAI_KEY}
+    url: https://api.openai.com/v1/chat/completions
+naver:
+  api:
+    id: ${NAVER_ID}
+    secret: ${NAVER_SECRET}
+    url: https://openapi.naver.com/v1/search/shop.json
\ No newline at end of file


Reviews:
user: login: yeonic
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T06:26:33Z
user: login: yeonic
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T06:29:17Z
user: login: yeonic
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T06:30:34Z
user: login: yeonic
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T06:33:52Z
user: login: k1130k
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T06:52:48Z
user: login: k1130k
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T06:53:10Z
user: login: k1130k
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T06:53:20Z
user: login: k1130k
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T06:59:02Z
user: login: k1130k
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T07:04:18Z
user: login: yeonic
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T10:13:28Z
user: login: k1130k
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T10:26:35Z
user: login: yeonic
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T01:42:14Z
user: login: yeonic
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T01:43:27Z
user: login: k1130k
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T02:19:06Z
user: login: k1130k
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T02:37:15Z
user: login: yeonic
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T04:03:06Z
user: login: k1130k
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T04:05:00Z
user: login: yeonic
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T05:05:36Z
user: login: yeonic
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T05:07:15Z

Review comments:
diff_hunk: @@ -0,0 +1,38 @@
+package com.fifteen.auction.global.scheduler;
+
+
+import com.fifteen.auction.domain.product.entity.Product;
+import com.fifteen.auction.domain.product.repository.ProductRepository;
+import com.fifteen.auction.domain.product.service.MarketPriceService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@RequiredArgsConstructor
+@Component
+public class PriceCacheUpdateScheduler {
+
+    private final RedisTemplate<String, Object> redisTemplate;
+    private final ProductRepository productRepository;
+    private final MarketPriceService marketPriceService;
+
+    @Scheduled(fixedRate = 60_000) // 매 1분마다 실행
+    public void refreshExpiredCacheProducts() {
+        System.out.println("[스케줄러 동작 중] 캐시 점검중..");
+
+        List<Product> products = productRepository.findAll();
+        for (Product product : products) {
+            String cacheKey = "price:" + product.getId();
+            Long ttl = redisTemplate.getExpire(cacheKey, TimeUnit.SECONDS);
path: src/main/java/com/fifteen/auction/global/scheduler/PriceCacheUpdateScheduler.java
user: login: yeonic
body: 캐시가 만료되는 녀석들을 점검하기 위해서 1분마다 findAll을 호출하는 건, 성능상의 문제가 있을 것 같습니다..!

@Cacheable을 이용한 Cache Aside 패턴을 사용해서 캐시에 값이 없으면 해당 값만 DB에서 가져와 업데이트 하도록 구현하면 어떨까요?
created_at: 2025-04-11T06:26:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+package com.fifteen.auction.global.client;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fifteen.auction.domain.product.dto.response.GPTPricePredictionResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+@Component
+@RequiredArgsConstructor
+public class OpenAIClient {
+
+    @Value("${openai.api.url}")
+    private String apiUrl;
+
+    @Value("${openai.api.key}")
+    private String apiKey;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    public List<GPTPricePredictionResponse> callGptForHistoricalPrices(String title, String description) {
+        LocalDate today = LocalDate.now();
+        List<LocalDate> dates = IntStream.rangeClosed(1, 3)
+                .mapToObj(i -> today.minusMonths(i).withDayOfMonth(1))
+                .collect(Collectors.toList());
+        Collections.reverse(dates);
+        dates.add(today); // 오늘 날짜 포함
+
+        StringBuilder exampleBuilder = new StringBuilder("[\n");
+        for (LocalDate date : dates) {
+            exampleBuilder.append(String.format("  { \"date\": \"%s\", \"min\": 30000, \"max\": 50000 },\n",
+                    date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))));
+        }
+        exampleBuilder.setLength(exampleBuilder.length() - 2);
+        exampleBuilder.append("\n]");
+
+        String prompt = String.format("""
+            다음 중고 상품의 최근 3개월 (1일 기준)과 오늘(%s)의 예상 거래 가격 범위를 알려줘.
+            형식은 다음과 같이 JSON 배열로 정확하게 반환해줘:
+
+            %s
+
+            제품명: %s
+            설명: %s
+            참고로 중고나라나 번개장터의 일반적인 거래 기준으로 알려줘.
+            """,
+                today.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),
+                exampleBuilder,
+                title,
+                description
+        );
+
+        Map<String, Object> request = new HashMap<>();
+        request.put("model", "gpt-3.5-turbo");
+        request.put("messages", List.of(Map.of("role", "user", "content", prompt)));
+        request.put("temperature", 0.7);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        headers.setBearerAuth(apiKey);
+
+        HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);
+        ResponseEntity<String> response = restTemplate.exchange(
+                apiUrl,
+                HttpMethod.POST,
+                entity,
+                String.class
+        );
+
path: src/main/java/com/fifteen/auction/global/client/OpenAIClient.java
user: login: yeonic
body: 대정님과 수환님도 외부 API로 요청 보내는 부분이 있는데, Openfeign 적용하시기로 했습니다..!

성능 개선 세션 때 OpenFeign도 한 번 알아보시면 좋을 것 같아요 :)
created_at: 2025-04-11T06:29:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,5 +3,15 @@
 import com.fifteen.auction.domain.product.entity.MarketPrice;
 import org.springframework.data.jpa.repository.JpaRepository;
 
+import java.time.LocalDate;
+import java.util.List;
+
 public interface MarketPriceRepository extends JpaRepository<MarketPrice, Long> {
+
+    // 품의 특정 날짜 시세가 이미 존재하는지 확인
path: src/main/java/com/fifteen/auction/domain/product/repository/MarketPriceRepository.java
user: login: yeonic
body: 여기 주석에서 글자 '상'이 날아간 것 같습니다
created_at: 2025-04-11T06:30:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,133 @@
+package com.fifteen.auction.domain.product.service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.fifteen.auction.domain.product.dto.response.GPTPricePredictionResponse;
+import com.fifteen.auction.domain.product.dto.response.MarketPriceFullResponse;
+import com.fifteen.auction.domain.product.dto.response.MarketPriceResponse;
+import com.fifteen.auction.domain.product.entity.MarketPrice;
+import com.fifteen.auction.domain.product.entity.Product;
+import com.fifteen.auction.domain.product.repository.MarketPriceRepository;
+import com.fifteen.auction.domain.product.repository.ProductRepository;
+import com.fifteen.auction.global.client.OpenAIClient;
+import com.fifteen.auction.global.dto.error.ErrorCode;
+import com.fifteen.auction.global.dto.exception.ServerException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDate;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+@Service
+@RequiredArgsConstructor
+public class MarketPriceService {
+
+    private final MarketPriceRepository marketPriceRepository;
+    private final ProductRepository productRepository;
+    private final OpenAIClient openAIClient;
+    private final RedisTemplate<String, Object> redisTemplateObject;
+    private static final String CACHE_PREFIX = "price:";
+    private static final long TTL_HOURS = 24L;
+    private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
+
+    //GPT로 시세 예측 → 최근 3개월 시세는 DB, 오늘은 Redis 캐시에 저장
+    public void predictAndSavePrice(Product product) {
+        List<GPTPricePredictionResponse> historicalPrices = openAIClient.callGptForHistoricalPrices(
+                product.getName(),
+                product.getDescription()
+        );
+
+        Set<LocalDate> savedDates = new HashSet<>(); // 중복날짜 저장 방지
+
+        for (int i = 0; i < historicalPrices.size(); i++) {
+            GPTPricePredictionResponse dto = historicalPrices.get(i);
+            LocalDate priceDate = LocalDate.parse(dto.getDate());
+
+            //  GPT 응답 내 중복 날짜 방지
+            if (!savedDates.add(priceDate)) continue;
+
+            boolean isToday = (i == historicalPrices.size() - 1);
+            boolean alreadySaved = marketPriceRepository.existsByProductIdAndPriceDate(product.getId(), priceDate);
+
+            if (!isToday && alreadySaved) continue;
+
+            MarketPrice price = MarketPrice.builder()
+                    .product(product)
+                    .priceDate(priceDate)
+                    .minMarketPrice(dto.getMin())
+                    .maxMarketPrice(dto.getMax())
+                    .build();
+
+            if (isToday) {
+                redisTemplateObject.opsForValue().set(
+                        CACHE_PREFIX + product.getId(),
+                        price,
+                        TTL_HOURS,
+                        TimeUnit.HOURS
+                );
+            } else {
+                marketPriceRepository.save(price);
+            }
+        }
+    }
+
+
+    //오늘 + 최근 3개월 시세조회
+    @Transactional(readOnly = true)
+    public MarketPriceFullResponse findMarketPriceFullResponse(Long productId) {
+        // 1. 오늘 시세 (Redis)
+        Object raw = redisTemplateObject.opsForValue().get(CACHE_PREFIX + productId);
+        MarketPrice todayPrice = null;
+
+        if (raw instanceof LinkedHashMap map) {
+            todayPrice = objectMapper.convertValue(map, MarketPrice.class);
+        } else if (raw instanceof MarketPrice cached) {
+            todayPrice = cached;
+        }
+
+        if (todayPrice == null) {
+            throw new ServerException(ErrorCode.MARKET_PRICE_NOT_FOUND);
+        }
+
+        MarketPriceResponse today = MarketPriceResponse.fromEntity(todayPrice);
+
+        // 2. DB에서 최근 3개월 시세
+        List<MarketPriceResponse> history = marketPriceRepository
+                .findAllByProductIdOrderByPriceDateAsc(productId)
+                .stream()
+                .map(MarketPriceResponse::fromEntity)
+                .toList();
+
+        return MarketPriceFullResponse.builder()
+                .todayPrice(today)
+                .historicalPrices(history)
+                .build();
+    }
+
+    @Scheduled(fixedRate = 60_000) // 1분마다 한번씩 실행
+    public void refreshExpiredCacheProducts() {
path: src/main/java/com/fifteen/auction/domain/product/service/MarketPriceService.java
user: login: yeonic
body: 이 메서드는 따로 파일로 빼신 것 같은데, 여기에도 남아있네요..? 혹시 이유가 있을까요?
created_at: 2025-04-11T06:33:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,133 @@
+package com.fifteen.auction.domain.product.service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.fifteen.auction.domain.product.dto.response.GPTPricePredictionResponse;
+import com.fifteen.auction.domain.product.dto.response.MarketPriceFullResponse;
+import com.fifteen.auction.domain.product.dto.response.MarketPriceResponse;
+import com.fifteen.auction.domain.product.entity.MarketPrice;
+import com.fifteen.auction.domain.product.entity.Product;
+import com.fifteen.auction.domain.product.repository.MarketPriceRepository;
+import com.fifteen.auction.domain.product.repository.ProductRepository;
+import com.fifteen.auction.global.client.OpenAIClient;
+import com.fifteen.auction.global.dto.error.ErrorCode;
+import com.fifteen.auction.global.dto.exception.ServerException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDate;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+@Service
+@RequiredArgsConstructor
+public class MarketPriceService {
+
+    private final MarketPriceRepository marketPriceRepository;
+    private final ProductRepository productRepository;
+    private final OpenAIClient openAIClient;
+    private final RedisTemplate<String, Object> redisTemplateObject;
+    private static final String CACHE_PREFIX = "price:";
+    private static final long TTL_HOURS = 24L;
+    private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
+
+    //GPT로 시세 예측 → 최근 3개월 시세는 DB, 오늘은 Redis 캐시에 저장
+    public void predictAndSavePrice(Product product) {
+        List<GPTPricePredictionResponse> historicalPrices = openAIClient.callGptForHistoricalPrices(
+                product.getName(),
+                product.getDescription()
+        );
+
+        Set<LocalDate> savedDates = new HashSet<>(); // 중복날짜 저장 방지
+
+        for (int i = 0; i < historicalPrices.size(); i++) {
+            GPTPricePredictionResponse dto = historicalPrices.get(i);
+            LocalDate priceDate = LocalDate.parse(dto.getDate());
+
+            //  GPT 응답 내 중복 날짜 방지
+            if (!savedDates.add(priceDate)) continue;
+
+            boolean isToday = (i == historicalPrices.size() - 1);
+            boolean alreadySaved = marketPriceRepository.existsByProductIdAndPriceDate(product.getId(), priceDate);
+
+            if (!isToday && alreadySaved) continue;
+
+            MarketPrice price = MarketPrice.builder()
+                    .product(product)
+                    .priceDate(priceDate)
+                    .minMarketPrice(dto.getMin())
+                    .maxMarketPrice(dto.getMax())
+                    .build();
+
+            if (isToday) {
+                redisTemplateObject.opsForValue().set(
+                        CACHE_PREFIX + product.getId(),
+                        price,
+                        TTL_HOURS,
+                        TimeUnit.HOURS
+                );
+            } else {
+                marketPriceRepository.save(price);
+            }
+        }
+    }
+
+
+    //오늘 + 최근 3개월 시세조회
+    @Transactional(readOnly = true)
+    public MarketPriceFullResponse findMarketPriceFullResponse(Long productId) {
+        // 1. 오늘 시세 (Redis)
+        Object raw = redisTemplateObject.opsForValue().get(CACHE_PREFIX + productId);
+        MarketPrice todayPrice = null;
+
+        if (raw instanceof LinkedHashMap map) {
+            todayPrice = objectMapper.convertValue(map, MarketPrice.class);
+        } else if (raw instanceof MarketPrice cached) {
+            todayPrice = cached;
+        }
+
+        if (todayPrice == null) {
+            throw new ServerException(ErrorCode.MARKET_PRICE_NOT_FOUND);
+        }
+
+        MarketPriceResponse today = MarketPriceResponse.fromEntity(todayPrice);
+
+        // 2. DB에서 최근 3개월 시세
+        List<MarketPriceResponse> history = marketPriceRepository
+                .findAllByProductIdOrderByPriceDateAsc(productId)
+                .stream()
+                .map(MarketPriceResponse::fromEntity)
+                .toList();
+
+        return MarketPriceFullResponse.builder()
+                .todayPrice(today)
+                .historicalPrices(history)
+                .build();
+    }
+
+    @Scheduled(fixedRate = 60_000) // 1분마다 한번씩 실행
+    public void refreshExpiredCacheProducts() {
path: src/main/java/com/fifteen/auction/domain/product/service/MarketPriceService.java
user: login: k1130k
body: 안그래도 이 부분 확인돼서 이미 제거를 했습니다 수정과정에서 잘못 가져와진 것 같습니다 ㅠ
created_at: 2025-04-11T06:52:48Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,98 @@
+package com.fifteen.auction.global.client;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fifteen.auction.domain.product.dto.response.GPTPricePredictionResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+@Component
+@RequiredArgsConstructor
+public class OpenAIClient {
+
+    @Value("${openai.api.url}")
+    private String apiUrl;
+
+    @Value("${openai.api.key}")
+    private String apiKey;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    public List<GPTPricePredictionResponse> callGptForHistoricalPrices(String title, String description) {
+        LocalDate today = LocalDate.now();
+        List<LocalDate> dates = IntStream.rangeClosed(1, 3)
+                .mapToObj(i -> today.minusMonths(i).withDayOfMonth(1))
+                .collect(Collectors.toList());
+        Collections.reverse(dates);
+        dates.add(today); // 오늘 날짜 포함
+
+        StringBuilder exampleBuilder = new StringBuilder("[\n");
+        for (LocalDate date : dates) {
+            exampleBuilder.append(String.format("  { \"date\": \"%s\", \"min\": 30000, \"max\": 50000 },\n",
+                    date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))));
+        }
+        exampleBuilder.setLength(exampleBuilder.length() - 2);
+        exampleBuilder.append("\n]");
+
+        String prompt = String.format("""
+            다음 중고 상품의 최근 3개월 (1일 기준)과 오늘(%s)의 예상 거래 가격 범위를 알려줘.
+            형식은 다음과 같이 JSON 배열로 정확하게 반환해줘:
+
+            %s
+
+            제품명: %s
+            설명: %s
+            참고로 중고나라나 번개장터의 일반적인 거래 기준으로 알려줘.
+            """,
+                today.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),
+                exampleBuilder,
+                title,
+                description
+        );
+
+        Map<String, Object> request = new HashMap<>();
+        request.put("model", "gpt-3.5-turbo");
+        request.put("messages", List.of(Map.of("role", "user", "content", prompt)));
+        request.put("temperature", 0.7);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        headers.setBearerAuth(apiKey);
+
+        HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);
+        ResponseEntity<String> response = restTemplate.exchange(
+                apiUrl,
+                HttpMethod.POST,
+                entity,
+                String.class
+        );
+
path: src/main/java/com/fifteen/auction/global/client/OpenAIClient.java
user: login: k1130k
body: 넵 학습 해보겠습니다 감사합니다~!
created_at: 2025-04-11T06:53:10Z
author_association: MEMBER
diff_hunk: @@ -3,5 +3,15 @@
 import com.fifteen.auction.domain.product.entity.MarketPrice;
 import org.springframework.data.jpa.repository.JpaRepository;
 
+import java.time.LocalDate;
+import java.util.List;
+
 public interface MarketPriceRepository extends JpaRepository<MarketPrice, Long> {
+
+    // 품의 특정 날짜 시세가 이미 존재하는지 확인
path: src/main/java/com/fifteen/auction/domain/product/repository/MarketPriceRepository.java
user: login: k1130k
body: 지적 감사합니다~
created_at: 2025-04-11T06:53:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,38 @@
+package com.fifteen.auction.global.scheduler;
+
+
+import com.fifteen.auction.domain.product.entity.Product;
+import com.fifteen.auction.domain.product.repository.ProductRepository;
+import com.fifteen.auction.domain.product.service.MarketPriceService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@RequiredArgsConstructor
+@Component
+public class PriceCacheUpdateScheduler {
+
+    private final RedisTemplate<String, Object> redisTemplate;
+    private final ProductRepository productRepository;
+    private final MarketPriceService marketPriceService;
+
+    @Scheduled(fixedRate = 60_000) // 매 1분마다 실행
+    public void refreshExpiredCacheProducts() {
+        System.out.println("[스케줄러 동작 중] 캐시 점검중..");
+
+        List<Product> products = productRepository.findAll();
+        for (Product product : products) {
+            String cacheKey = "price:" + product.getId();
+            Long ttl = redisTemplate.getExpire(cacheKey, TimeUnit.SECONDS);
path: src/main/java/com/fifteen/auction/global/scheduler/PriceCacheUpdateScheduler.java
user: login: k1130k
body: 안그래도 이 부분 추후에 성능개선을 할 때 리펙토링할까하다가 일단은 레디스에서 캐싱된 productId를 가져온 뒤 캐싱만료된 상품을 걸러내서 캐싱하는 쪽으로 리펙토링을 했습니다 아직 푸시가 안된상태라 다시 푸시하고 코멘트 남기겠습니다~!
created_at: 2025-04-11T06:59:02Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,38 @@
+package com.fifteen.auction.global.scheduler;
+
+
+import com.fifteen.auction.domain.product.entity.Product;
+import com.fifteen.auction.domain.product.repository.ProductRepository;
+import com.fifteen.auction.domain.product.service.MarketPriceService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@RequiredArgsConstructor
+@Component
+public class PriceCacheUpdateScheduler {
+
+    private final RedisTemplate<String, Object> redisTemplate;
+    private final ProductRepository productRepository;
+    private final MarketPriceService marketPriceService;
+
+    @Scheduled(fixedRate = 60_000) // 매 1분마다 실행
+    public void refreshExpiredCacheProducts() {
+        System.out.println("[스케줄러 동작 중] 캐시 점검중..");
+
+        List<Product> products = productRepository.findAll();
+        for (Product product : products) {
+            String cacheKey = "price:" + product.getId();
+            Long ttl = redisTemplate.getExpire(cacheKey, TimeUnit.SECONDS);
path: src/main/java/com/fifteen/auction/global/scheduler/PriceCacheUpdateScheduler.java
user: login: k1130k
body: 최신화했으니 확인 부탁드립니다 말씀주신 방법이 더 좋다고 판단되시면 코멘트 부탁드립니다 반영하겠습니다!
created_at: 2025-04-11T07:04:18Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,38 @@
+package com.fifteen.auction.global.scheduler;
+
+
+import com.fifteen.auction.domain.product.entity.Product;
+import com.fifteen.auction.domain.product.repository.ProductRepository;
+import com.fifteen.auction.domain.product.service.MarketPriceService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@RequiredArgsConstructor
+@Component
+public class PriceCacheUpdateScheduler {
+
+    private final RedisTemplate<String, Object> redisTemplate;
+    private final ProductRepository productRepository;
+    private final MarketPriceService marketPriceService;
+
+    @Scheduled(fixedRate = 60_000) // 매 1분마다 실행
+    public void refreshExpiredCacheProducts() {
+        System.out.println("[스케줄러 동작 중] 캐시 점검중..");
+
+        List<Product> products = productRepository.findAll();
+        for (Product product : products) {
+            String cacheKey = "price:" + product.getId();
+            Long ttl = redisTemplate.getExpire(cacheKey, TimeUnit.SECONDS);
path: src/main/java/com/fifteen/auction/global/scheduler/PriceCacheUpdateScheduler.java
user: login: yeonic
body: 전보다는 효율적으로 바뀐 것 같은데, 제가 느끼기에는 매 1분마다 모든 상품의 시세를 업데이트 해주는 게 다소 비효율적으로 다가옵니다..

그렇게 느껴지는 포인트는 다음과 같습니다!!

1. 캐시는 '조회 속도-데이터의 정합성' 간의 trade-off에서 조회 속도를 택한 거기 때문에, 데이터의 정합성을 위해서 1분마다 만료된 캐시에 다시 적재를 해주는 건, 그 이점을 사용하지 못하는 것과 같다는 생각입니다.
2. 그리고 @Cacheable을 사용하는 구현이 간단하기도 합니다....
3. 이 구현은 조회 요청된 건에 대해서만 캐시 존재 여부를 판단해서 업데이트해서, 사용자 경험은 크게 차이나지 않지만 자원 활용 면에서 효율적입니다..

너무 와다다다 얘기해 버린 감이 있네요... 일단 제 의견입니다...

아래는 참고 자료입니다!
https://velog.io/@ryuneng2/Redis-Cacheable%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-Cache-Aside-%EC%A0%84%EB%9E%B5
created_at: 2025-04-11T10:13:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+package com.fifteen.auction.global.scheduler;
+
+
+import com.fifteen.auction.domain.product.entity.Product;
+import com.fifteen.auction.domain.product.repository.ProductRepository;
+import com.fifteen.auction.domain.product.service.MarketPriceService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@RequiredArgsConstructor
+@Component
+public class PriceCacheUpdateScheduler {
+
+    private final RedisTemplate<String, Object> redisTemplate;
+    private final ProductRepository productRepository;
+    private final MarketPriceService marketPriceService;
+
+    @Scheduled(fixedRate = 60_000) // 매 1분마다 실행
+    public void refreshExpiredCacheProducts() {
+        System.out.println("[스케줄러 동작 중] 캐시 점검중..");
+
+        List<Product> products = productRepository.findAll();
+        for (Product product : products) {
+            String cacheKey = "price:" + product.getId();
+            Long ttl = redisTemplate.getExpire(cacheKey, TimeUnit.SECONDS);
path: src/main/java/com/fifteen/auction/global/scheduler/PriceCacheUpdateScheduler.java
user: login: k1130k
body: 그러네요 Cacheable로 구현하는게 더 나아보이네요! 참고해서 수정해보겠습니다 자료감사합니다!
created_at: 2025-04-11T10:26:35Z
author_association: MEMBER
diff_hunk: @@ -66,6 +66,9 @@ public enum ErrorCode {
     // Market Price Exceptions
     MARKET_PRICE_NOT_FOUND(HttpStatus.NOT_FOUND, "MARKETPRICE-1", "시세 정보가 존재하지 않습니다."),
 
+    // Naver API Exceptions
+    EXTERNAL_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "API-1", "네이버 오픈 API 호출 중 오류가 발생했습니다."),
path: src/main/java/com/fifteen/auction/global/dto/error/ErrorCode.java
user: login: yeonic
body: 사용자에게 노출하지 않아도 되는 정보인 것 같습니다..!
created_at: 2025-04-18T01:42:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,57 +30,122 @@ public class OpenAIClient {
     private String apiKey;
 
     private final RestTemplate restTemplate = new RestTemplate();
+    private final NaverSearchClient naverSearchClient;
 
     public List<GPTPricePredictionResponse> callGptForHistoricalPrices(String title, String description) {
+        log.info("상품 '{}' 시세 예측 시작 - 네이버 쇼핑 API 호출", title);
+        List<NaverShoppingItemDto> shoppingItems = naverSearchClient.findShoppingItems(title);
+
+        String marketSummary = null;
+
+        if (!shoppingItems.isEmpty()) {
+            log.info("상품 '{}' 네이버 검색 결과 {}개 항목 - GPT 예측 시작", title, shoppingItems.size());
+
+            long minPrice = Long.MAX_VALUE;
+            long maxPrice = 0L;
+            long totalPrice = 0L;
+            int validItemCount = 0;
+
+            StringBuilder summary = new StringBuilder();
+            summary.append("현재 최신 시장 데이터 (네이버 쇼핑):\n");
+
+            for (NaverShoppingItemDto item : shoppingItems) {
+                if (item.getLprice() != null && item.getLprice() > 0) {
+                    minPrice = Math.min(minPrice, item.getLprice());
+                    maxPrice = Math.max(maxPrice, item.getLprice());
+                    totalPrice += item.getLprice();
+                    validItemCount++;
+
+                    summary.append(String.format("- %s: %,d원 (판매처: %s)\n",
+                            item.getTitle().replaceAll("<[^>]*>", ""),
+                            item.getLprice(),
+                            item.getMallName()));
+
+                    if (validItemCount >= 10) break;
+                }
+            }
+
+            if (validItemCount > 0) {
+                summary.append(String.format("\n현재 최저가: %,d원\n", minPrice));
+                summary.append(String.format("현재 최고가: %,d원\n", maxPrice));
+                summary.append(String.format("현재 평균가: %,d원\n", totalPrice / validItemCount));
+                marketSummary = summary.toString();
+            }
+        }
+
+        if (shoppingItems.isEmpty() || marketSummary == null) {
+            log.info("상품 '{}' 네이버 검색 결과 없음 또는 유효하지 않음 - ChatGPT를 사용한 예측으로 전환", title);
+        }
+
+        String prompt = buildPrompt(title, description, marketSummary);
+        return callGptWithPrompt(prompt);
+    }
+
+    private String buildPrompt(String title, String description, String marketSummary) {
         LocalDate today = LocalDate.now();
         List<LocalDate> dates = IntStream.rangeClosed(1, 3)
                 .mapToObj(i -> today.minusMonths(i).withDayOfMonth(1))
                 .collect(Collectors.toList());
         Collections.reverse(dates);
-        dates.add(today); // 오늘 날짜 포함
+        dates.add(today); // 오늘 포함
 
-        StringBuilder exampleBuilder = new StringBuilder("[\n");
-        for (LocalDate date : dates) {
-            exampleBuilder.append(String.format("  { \"date\": \"%s\", \"min\": 30000, \"max\": 50000 },\n",
-                    date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))));
-        }
-        exampleBuilder.setLength(exampleBuilder.length() - 2);
-        exampleBuilder.append("\n]");
+        String exampleJson = buildExampleJsonFormat(dates);
 
-        String prompt = String.format("""
+        return String.format("""
             다음 중고 상품의 최근 3개월 (1일 기준)과 오늘(%s)의 예상 거래 가격 범위를 알려줘.
             형식은 다음과 같이 JSON 배열로 정확하게 반환해줘:
 
+            ※ 가격 숫자는 쉼표(,) 없이 정수로만 반환해주고 
+            모든 key는 반드시 따옴표로 감싸줘.
+             value는 숫자로 주고 절대 다른 텍스트 포함하지마
+
             %s
 
+            %s
             제품명: %s
             설명: %s
-            참고로 중고나라나 번개장터의 일반적인 거래 기준으로 알려줘.
             """,
                 today.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),
-                exampleBuilder,
+                exampleJson,
+                marketSummary != null ? marketSummary + "\n" : "",
                 title,
                 description
         );
+    }
+
+    private String buildExampleJsonFormat(List<LocalDate> dates) {
+        StringBuilder exampleBuilder = new StringBuilder("[\n");
+        for (LocalDate date : dates) {
+            exampleBuilder.append(String.format("  { \"date\": \"%s\", \"min\": 최저가, \"max\": 최고가 },\n",
+                    date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))));
+        }
+        exampleBuilder.setLength(exampleBuilder.length() - 2); // 마지막 콤마 제거
+        exampleBuilder.append("\n]");
+        return exampleBuilder.toString();
+    }
 
+    private List<GPTPricePredictionResponse> callGptWithPrompt(String prompt) {
         Map<String, Object> request = new HashMap<>();
         request.put("model", "gpt-3.5-turbo");
         request.put("messages", List.of(Map.of("role", "user", "content", prompt)));
-        request.put("temperature", 0.7);
+        request.put("temperature", 0.5);
path: src/main/java/com/fifteen/auction/global/client/OpenAIClient.java
user: login: yeonic
body: 여기 hashmap으로 생성되는 메시지는 객체로 관리가 가능할 것 같습니다..!
created_at: 2025-04-18T01:43:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,6 +66,9 @@ public enum ErrorCode {
     // Market Price Exceptions
     MARKET_PRICE_NOT_FOUND(HttpStatus.NOT_FOUND, "MARKETPRICE-1", "시세 정보가 존재하지 않습니다."),
 
+    // Naver API Exceptions
+    EXTERNAL_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "API-1", "네이버 오픈 API 호출 중 오류가 발생했습니다."),
path: src/main/java/com/fifteen/auction/global/dto/error/ErrorCode.java
user: login: k1130k
body: 수정하겠습니다!
created_at: 2025-04-18T02:19:06Z
author_association: MEMBER
diff_hunk: @@ -29,57 +30,122 @@ public class OpenAIClient {
     private String apiKey;
 
     private final RestTemplate restTemplate = new RestTemplate();
+    private final NaverSearchClient naverSearchClient;
 
     public List<GPTPricePredictionResponse> callGptForHistoricalPrices(String title, String description) {
+        log.info("상품 '{}' 시세 예측 시작 - 네이버 쇼핑 API 호출", title);
+        List<NaverShoppingItemDto> shoppingItems = naverSearchClient.findShoppingItems(title);
+
+        String marketSummary = null;
+
+        if (!shoppingItems.isEmpty()) {
+            log.info("상품 '{}' 네이버 검색 결과 {}개 항목 - GPT 예측 시작", title, shoppingItems.size());
+
+            long minPrice = Long.MAX_VALUE;
+            long maxPrice = 0L;
+            long totalPrice = 0L;
+            int validItemCount = 0;
+
+            StringBuilder summary = new StringBuilder();
+            summary.append("현재 최신 시장 데이터 (네이버 쇼핑):\n");
+
+            for (NaverShoppingItemDto item : shoppingItems) {
+                if (item.getLprice() != null && item.getLprice() > 0) {
+                    minPrice = Math.min(minPrice, item.getLprice());
+                    maxPrice = Math.max(maxPrice, item.getLprice());
+                    totalPrice += item.getLprice();
+                    validItemCount++;
+
+                    summary.append(String.format("- %s: %,d원 (판매처: %s)\n",
+                            item.getTitle().replaceAll("<[^>]*>", ""),
+                            item.getLprice(),
+                            item.getMallName()));
+
+                    if (validItemCount >= 10) break;
+                }
+            }
+
+            if (validItemCount > 0) {
+                summary.append(String.format("\n현재 최저가: %,d원\n", minPrice));
+                summary.append(String.format("현재 최고가: %,d원\n", maxPrice));
+                summary.append(String.format("현재 평균가: %,d원\n", totalPrice / validItemCount));
+                marketSummary = summary.toString();
+            }
+        }
+
+        if (shoppingItems.isEmpty() || marketSummary == null) {
+            log.info("상품 '{}' 네이버 검색 결과 없음 또는 유효하지 않음 - ChatGPT를 사용한 예측으로 전환", title);
+        }
+
+        String prompt = buildPrompt(title, description, marketSummary);
+        return callGptWithPrompt(prompt);
+    }
+
+    private String buildPrompt(String title, String description, String marketSummary) {
         LocalDate today = LocalDate.now();
         List<LocalDate> dates = IntStream.rangeClosed(1, 3)
                 .mapToObj(i -> today.minusMonths(i).withDayOfMonth(1))
                 .collect(Collectors.toList());
         Collections.reverse(dates);
-        dates.add(today); // 오늘 날짜 포함
+        dates.add(today); // 오늘 포함
 
-        StringBuilder exampleBuilder = new StringBuilder("[\n");
-        for (LocalDate date : dates) {
-            exampleBuilder.append(String.format("  { \"date\": \"%s\", \"min\": 30000, \"max\": 50000 },\n",
-                    date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))));
-        }
-        exampleBuilder.setLength(exampleBuilder.length() - 2);
-        exampleBuilder.append("\n]");
+        String exampleJson = buildExampleJsonFormat(dates);
 
-        String prompt = String.format("""
+        return String.format("""
             다음 중고 상품의 최근 3개월 (1일 기준)과 오늘(%s)의 예상 거래 가격 범위를 알려줘.
             형식은 다음과 같이 JSON 배열로 정확하게 반환해줘:
 
+            ※ 가격 숫자는 쉼표(,) 없이 정수로만 반환해주고 
+            모든 key는 반드시 따옴표로 감싸줘.
+             value는 숫자로 주고 절대 다른 텍스트 포함하지마
+
             %s
 
+            %s
             제품명: %s
             설명: %s
-            참고로 중고나라나 번개장터의 일반적인 거래 기준으로 알려줘.
             """,
                 today.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),
-                exampleBuilder,
+                exampleJson,
+                marketSummary != null ? marketSummary + "\n" : "",
                 title,
                 description
         );
+    }
+
+    private String buildExampleJsonFormat(List<LocalDate> dates) {
+        StringBuilder exampleBuilder = new StringBuilder("[\n");
+        for (LocalDate date : dates) {
+            exampleBuilder.append(String.format("  { \"date\": \"%s\", \"min\": 최저가, \"max\": 최고가 },\n",
+                    date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))));
+        }
+        exampleBuilder.setLength(exampleBuilder.length() - 2); // 마지막 콤마 제거
+        exampleBuilder.append("\n]");
+        return exampleBuilder.toString();
+    }
 
+    private List<GPTPricePredictionResponse> callGptWithPrompt(String prompt) {
         Map<String, Object> request = new HashMap<>();
         request.put("model", "gpt-3.5-turbo");
         request.put("messages", List.of(Map.of("role", "user", "content", prompt)));
-        request.put("temperature", 0.7);
+        request.put("temperature", 0.5);
path: src/main/java/com/fifteen/auction/global/client/OpenAIClient.java
user: login: k1130k
body: 넵 수정해보겠습니다 !
created_at: 2025-04-18T02:37:15Z
author_association: MEMBER
diff_hunk: @@ -29,57 +30,122 @@ public class OpenAIClient {
     private String apiKey;
 
     private final RestTemplate restTemplate = new RestTemplate();
+    private final NaverSearchClient naverSearchClient;
 
     public List<GPTPricePredictionResponse> callGptForHistoricalPrices(String title, String description) {
+        log.info("상품 '{}' 시세 예측 시작 - 네이버 쇼핑 API 호출", title);
+        List<NaverShoppingItemDto> shoppingItems = naverSearchClient.findShoppingItems(title);
+
+        String marketSummary = null;
+
+        if (!shoppingItems.isEmpty()) {
+            log.info("상품 '{}' 네이버 검색 결과 {}개 항목 - GPT 예측 시작", title, shoppingItems.size());
+
+            long minPrice = Long.MAX_VALUE;
+            long maxPrice = 0L;
+            long totalPrice = 0L;
+            int validItemCount = 0;
+
+            StringBuilder summary = new StringBuilder();
+            summary.append("현재 최신 시장 데이터 (네이버 쇼핑):\n");
+
+            for (NaverShoppingItemDto item : shoppingItems) {
+                if (item.getLprice() != null && item.getLprice() > 0) {
+                    minPrice = Math.min(minPrice, item.getLprice());
+                    maxPrice = Math.max(maxPrice, item.getLprice());
+                    totalPrice += item.getLprice();
+                    validItemCount++;
+
+                    summary.append(String.format("- %s: %,d원 (판매처: %s)\n",
+                            item.getTitle().replaceAll("<[^>]*>", ""),
+                            item.getLprice(),
+                            item.getMallName()));
+
+                    if (validItemCount >= 10) break;
+                }
+            }
+
+            if (validItemCount > 0) {
+                summary.append(String.format("\n현재 최저가: %,d원\n", minPrice));
+                summary.append(String.format("현재 최고가: %,d원\n", maxPrice));
+                summary.append(String.format("현재 평균가: %,d원\n", totalPrice / validItemCount));
+                marketSummary = summary.toString();
+            }
+        }
+
+        if (shoppingItems.isEmpty() || marketSummary == null) {
+            log.info("상품 '{}' 네이버 검색 결과 없음 또는 유효하지 않음 - ChatGPT를 사용한 예측으로 전환", title);
+        }
+
+        String prompt = buildPrompt(title, description, marketSummary);
+        return callGptWithPrompt(prompt);
+    }
+
+    private String buildPrompt(String title, String description, String marketSummary) {
         LocalDate today = LocalDate.now();
         List<LocalDate> dates = IntStream.rangeClosed(1, 3)
                 .mapToObj(i -> today.minusMonths(i).withDayOfMonth(1))
                 .collect(Collectors.toList());
         Collections.reverse(dates);
-        dates.add(today); // 오늘 날짜 포함
+        dates.add(today); // 오늘 포함
 
-        StringBuilder exampleBuilder = new StringBuilder("[\n");
-        for (LocalDate date : dates) {
-            exampleBuilder.append(String.format("  { \"date\": \"%s\", \"min\": 30000, \"max\": 50000 },\n",
-                    date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))));
-        }
-        exampleBuilder.setLength(exampleBuilder.length() - 2);
-        exampleBuilder.append("\n]");
+        String exampleJson = buildExampleJsonFormat(dates);
 
-        String prompt = String.format("""
+        return String.format("""
             다음 중고 상품의 최근 3개월 (1일 기준)과 오늘(%s)의 예상 거래 가격 범위를 알려줘.
             형식은 다음과 같이 JSON 배열로 정확하게 반환해줘:
 
+            ※ 가격 숫자는 쉼표(,) 없이 정수로만 반환해주고 
+            모든 key는 반드시 따옴표로 감싸줘.
+             value는 숫자로 주고 절대 다른 텍스트 포함하지마
+
             %s
 
+            %s
             제품명: %s
             설명: %s
-            참고로 중고나라나 번개장터의 일반적인 거래 기준으로 알려줘.
             """,
                 today.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),
-                exampleBuilder,
+                exampleJson,
+                marketSummary != null ? marketSummary + "\n" : "",
                 title,
                 description
         );
+    }
+
+    private String buildExampleJsonFormat(List<LocalDate> dates) {
+        StringBuilder exampleBuilder = new StringBuilder("[\n");
+        for (LocalDate date : dates) {
+            exampleBuilder.append(String.format("  { \"date\": \"%s\", \"min\": 최저가, \"max\": 최고가 },\n",
+                    date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))));
+        }
+        exampleBuilder.setLength(exampleBuilder.length() - 2); // 마지막 콤마 제거
+        exampleBuilder.append("\n]");
+        return exampleBuilder.toString();
+    }
 
+    private List<GPTPricePredictionResponse> callGptWithPrompt(String prompt) {
         Map<String, Object> request = new HashMap<>();
         request.put("model", "gpt-3.5-turbo");
         request.put("messages", List.of(Map.of("role", "user", "content", prompt)));
-        request.put("temperature", 0.7);
+        request.put("temperature", 0.5);
path: src/main/java/com/fifteen/auction/global/client/OpenAIClient.java
user: login: yeonic
body: 수정이 되고 머지할까요?
created_at: 2025-04-18T04:03:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,57 +30,122 @@ public class OpenAIClient {
     private String apiKey;
 
     private final RestTemplate restTemplate = new RestTemplate();
+    private final NaverSearchClient naverSearchClient;
 
     public List<GPTPricePredictionResponse> callGptForHistoricalPrices(String title, String description) {
+        log.info("상품 '{}' 시세 예측 시작 - 네이버 쇼핑 API 호출", title);
+        List<NaverShoppingItemDto> shoppingItems = naverSearchClient.findShoppingItems(title);
+
+        String marketSummary = null;
+
+        if (!shoppingItems.isEmpty()) {
+            log.info("상품 '{}' 네이버 검색 결과 {}개 항목 - GPT 예측 시작", title, shoppingItems.size());
+
+            long minPrice = Long.MAX_VALUE;
+            long maxPrice = 0L;
+            long totalPrice = 0L;
+            int validItemCount = 0;
+
+            StringBuilder summary = new StringBuilder();
+            summary.append("현재 최신 시장 데이터 (네이버 쇼핑):\n");
+
+            for (NaverShoppingItemDto item : shoppingItems) {
+                if (item.getLprice() != null && item.getLprice() > 0) {
+                    minPrice = Math.min(minPrice, item.getLprice());
+                    maxPrice = Math.max(maxPrice, item.getLprice());
+                    totalPrice += item.getLprice();
+                    validItemCount++;
+
+                    summary.append(String.format("- %s: %,d원 (판매처: %s)\n",
+                            item.getTitle().replaceAll("<[^>]*>", ""),
+                            item.getLprice(),
+                            item.getMallName()));
+
+                    if (validItemCount >= 10) break;
+                }
+            }
+
+            if (validItemCount > 0) {
+                summary.append(String.format("\n현재 최저가: %,d원\n", minPrice));
+                summary.append(String.format("현재 최고가: %,d원\n", maxPrice));
+                summary.append(String.format("현재 평균가: %,d원\n", totalPrice / validItemCount));
+                marketSummary = summary.toString();
+            }
+        }
+
+        if (shoppingItems.isEmpty() || marketSummary == null) {
+            log.info("상품 '{}' 네이버 검색 결과 없음 또는 유효하지 않음 - ChatGPT를 사용한 예측으로 전환", title);
+        }
+
+        String prompt = buildPrompt(title, description, marketSummary);
+        return callGptWithPrompt(prompt);
+    }
+
+    private String buildPrompt(String title, String description, String marketSummary) {
         LocalDate today = LocalDate.now();
         List<LocalDate> dates = IntStream.rangeClosed(1, 3)
                 .mapToObj(i -> today.minusMonths(i).withDayOfMonth(1))
                 .collect(Collectors.toList());
         Collections.reverse(dates);
-        dates.add(today); // 오늘 날짜 포함
+        dates.add(today); // 오늘 포함
 
-        StringBuilder exampleBuilder = new StringBuilder("[\n");
-        for (LocalDate date : dates) {
-            exampleBuilder.append(String.format("  { \"date\": \"%s\", \"min\": 30000, \"max\": 50000 },\n",
-                    date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))));
-        }
-        exampleBuilder.setLength(exampleBuilder.length() - 2);
-        exampleBuilder.append("\n]");
+        String exampleJson = buildExampleJsonFormat(dates);
 
-        String prompt = String.format("""
+        return String.format("""
             다음 중고 상품의 최근 3개월 (1일 기준)과 오늘(%s)의 예상 거래 가격 범위를 알려줘.
             형식은 다음과 같이 JSON 배열로 정확하게 반환해줘:
 
+            ※ 가격 숫자는 쉼표(,) 없이 정수로만 반환해주고 
+            모든 key는 반드시 따옴표로 감싸줘.
+             value는 숫자로 주고 절대 다른 텍스트 포함하지마
+
             %s
 
+            %s
             제품명: %s
             설명: %s
-            참고로 중고나라나 번개장터의 일반적인 거래 기준으로 알려줘.
             """,
                 today.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),
-                exampleBuilder,
+                exampleJson,
+                marketSummary != null ? marketSummary + "\n" : "",
                 title,
                 description
         );
+    }
+
+    private String buildExampleJsonFormat(List<LocalDate> dates) {
+        StringBuilder exampleBuilder = new StringBuilder("[\n");
+        for (LocalDate date : dates) {
+            exampleBuilder.append(String.format("  { \"date\": \"%s\", \"min\": 최저가, \"max\": 최고가 },\n",
+                    date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))));
+        }
+        exampleBuilder.setLength(exampleBuilder.length() - 2); // 마지막 콤마 제거
+        exampleBuilder.append("\n]");
+        return exampleBuilder.toString();
+    }
 
+    private List<GPTPricePredictionResponse> callGptWithPrompt(String prompt) {
         Map<String, Object> request = new HashMap<>();
         request.put("model", "gpt-3.5-turbo");
         request.put("messages", List.of(Map.of("role", "user", "content", prompt)));
-        request.put("temperature", 0.7);
+        request.put("temperature", 0.5);
path: src/main/java/com/fifteen/auction/global/client/OpenAIClient.java
user: login: k1130k
body: 먼저 머지를 해도 상관없는데 어떻게 생각하시나요? 수정하려면 시간이 소요될 것 같아서요~
created_at: 2025-04-18T04:05:00Z
author_association: MEMBER
diff_hunk: @@ -29,57 +30,122 @@ public class OpenAIClient {
     private String apiKey;
 
     private final RestTemplate restTemplate = new RestTemplate();
+    private final NaverSearchClient naverSearchClient;
 
     public List<GPTPricePredictionResponse> callGptForHistoricalPrices(String title, String description) {
+        log.info("상품 '{}' 시세 예측 시작 - 네이버 쇼핑 API 호출", title);
+        List<NaverShoppingItemDto> shoppingItems = naverSearchClient.findShoppingItems(title);
+
+        String marketSummary = null;
+
+        if (!shoppingItems.isEmpty()) {
+            log.info("상품 '{}' 네이버 검색 결과 {}개 항목 - GPT 예측 시작", title, shoppingItems.size());
+
+            long minPrice = Long.MAX_VALUE;
+            long maxPrice = 0L;
+            long totalPrice = 0L;
+            int validItemCount = 0;
+
+            StringBuilder summary = new StringBuilder();
+            summary.append("현재 최신 시장 데이터 (네이버 쇼핑):\n");
+
+            for (NaverShoppingItemDto item : shoppingItems) {
+                if (item.getLprice() != null && item.getLprice() > 0) {
+                    minPrice = Math.min(minPrice, item.getLprice());
+                    maxPrice = Math.max(maxPrice, item.getLprice());
+                    totalPrice += item.getLprice();
+                    validItemCount++;
+
+                    summary.append(String.format("- %s: %,d원 (판매처: %s)\n",
+                            item.getTitle().replaceAll("<[^>]*>", ""),
+                            item.getLprice(),
+                            item.getMallName()));
+
+                    if (validItemCount >= 10) break;
+                }
+            }
+
+            if (validItemCount > 0) {
+                summary.append(String.format("\n현재 최저가: %,d원\n", minPrice));
+                summary.append(String.format("현재 최고가: %,d원\n", maxPrice));
+                summary.append(String.format("현재 평균가: %,d원\n", totalPrice / validItemCount));
+                marketSummary = summary.toString();
+            }
+        }
+
+        if (shoppingItems.isEmpty() || marketSummary == null) {
+            log.info("상품 '{}' 네이버 검색 결과 없음 또는 유효하지 않음 - ChatGPT를 사용한 예측으로 전환", title);
+        }
+
+        String prompt = buildPrompt(title, description, marketSummary);
+        return callGptWithPrompt(prompt);
+    }
+
+    private String buildPrompt(String title, String description, String marketSummary) {
         LocalDate today = LocalDate.now();
         List<LocalDate> dates = IntStream.rangeClosed(1, 3)
                 .mapToObj(i -> today.minusMonths(i).withDayOfMonth(1))
                 .collect(Collectors.toList());
         Collections.reverse(dates);
-        dates.add(today); // 오늘 날짜 포함
+        dates.add(today); // 오늘 포함
 
-        StringBuilder exampleBuilder = new StringBuilder("[\n");
-        for (LocalDate date : dates) {
-            exampleBuilder.append(String.format("  { \"date\": \"%s\", \"min\": 30000, \"max\": 50000 },\n",
-                    date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))));
-        }
-        exampleBuilder.setLength(exampleBuilder.length() - 2);
-        exampleBuilder.append("\n]");
+        String exampleJson = buildExampleJsonFormat(dates);
 
-        String prompt = String.format("""
+        return String.format("""
             다음 중고 상품의 최근 3개월 (1일 기준)과 오늘(%s)의 예상 거래 가격 범위를 알려줘.
             형식은 다음과 같이 JSON 배열로 정확하게 반환해줘:
 
+            ※ 가격 숫자는 쉼표(,) 없이 정수로만 반환해주고 
+            모든 key는 반드시 따옴표로 감싸줘.
+             value는 숫자로 주고 절대 다른 텍스트 포함하지마
+
             %s
 
+            %s
             제품명: %s
             설명: %s
-            참고로 중고나라나 번개장터의 일반적인 거래 기준으로 알려줘.
             """,
                 today.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),
-                exampleBuilder,
+                exampleJson,
+                marketSummary != null ? marketSummary + "\n" : "",
                 title,
                 description
         );
+    }
+
+    private String buildExampleJsonFormat(List<LocalDate> dates) {
+        StringBuilder exampleBuilder = new StringBuilder("[\n");
+        for (LocalDate date : dates) {
+            exampleBuilder.append(String.format("  { \"date\": \"%s\", \"min\": 최저가, \"max\": 최고가 },\n",
+                    date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))));
+        }
+        exampleBuilder.setLength(exampleBuilder.length() - 2); // 마지막 콤마 제거
+        exampleBuilder.append("\n]");
+        return exampleBuilder.toString();
+    }
 
+    private List<GPTPricePredictionResponse> callGptWithPrompt(String prompt) {
         Map<String, Object> request = new HashMap<>();
         request.put("model", "gpt-3.5-turbo");
         request.put("messages", List.of(Map.of("role", "user", "content", prompt)));
-        request.put("temperature", 0.7);
+        request.put("temperature", 0.5);
path: src/main/java/com/fifteen/auction/global/client/OpenAIClient.java
user: login: yeonic
body: 머지하겠습니다 :)
created_at: 2025-04-18T05:05:36Z
author_association: CONTRIBUTOR

PR comments:
user: login: yeonic
created_at: 2025-04-11T06:35:06Z
body: 구현하시느라 정말 고생 많으셨습니다..! 
외부 API 익히고 사용하는게 어려운 작업인데, 정말 노력 많이 하신 것 같습니다!!

- Scheduler 활용하신 부분은 코멘트 드린 대로 한 번 생각해보시면 더 좋을 것 같습니다 👍 👍 
author_association: CONTRIBUTOR
user: login: Doritos38
created_at: 2025-04-16T12:03:30Z
body: pr이 올라간 상태로 push하셔서 충돌이 생긴 것 같습니다. 작성된 코드를 보며 openAI api 적용에 대해 공부할 수 있어서 좋았습니다 고생하셨습니다!!
author_association: CONTRIBUTOR
user: login: yeonic
created_at: 2025-04-18T01:45:08Z
body: 구현하시느라 고생 많으셨습니다...! 많은 노력이 들어간 느낌이 나서 보기 송구합니다🥲
author_association: CONTRIBUTOR