diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 6e8a50c..d816a54 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -57,6 +57,9 @@ jobs: spring.servlet.multipart.enabled=true spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=10MB + + naver.client.id=${{ secrets.NAVER_ID }} + naver.client.secret=${{ secrets.NAVER_SECRET }} EOT shell: bash diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 8865dc5..2e0abac 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -55,6 +55,9 @@ jobs: spring.servlet.multipart.enabled=true spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=10MB + + naver.client.id=${{ secrets.NAVER_ID }} + naver.client.secret=${{ secrets.NAVER_SECRET }} EOT shell: bash diff --git a/build.gradle b/build.gradle index 39bc28e..1b6b02e 100644 --- a/build.gradle +++ b/build.gradle @@ -62,6 +62,8 @@ dependencies { annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" + + implementation 'org.json:json:20210307' } // Querydsl 설정부 diff --git a/src/main/generated/kw/zeropick/product/domain/QProduct.java b/src/main/generated/kw/zeropick/product/domain/QProduct.java index dcba5de..71ac4ce 100644 --- a/src/main/generated/kw/zeropick/product/domain/QProduct.java +++ b/src/main/generated/kw/zeropick/product/domain/QProduct.java @@ -53,6 +53,8 @@ public class QProduct extends EntityPathBase { public final NumberPath price = createNumber("price", Integer.class); + public final StringPath productLink = createString("productLink"); + public final StringPath productName = createString("productName"); public final NumberPath reviewCount = createNumber("reviewCount", Integer.class); diff --git a/src/main/java/kw/zeropick/config/RestTemplateConfig.java b/src/main/java/kw/zeropick/config/RestTemplateConfig.java new file mode 100644 index 0000000..de6528e --- /dev/null +++ b/src/main/java/kw/zeropick/config/RestTemplateConfig.java @@ -0,0 +1,13 @@ +package kw.zeropick.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/src/main/java/kw/zeropick/product/controller/ProductSearchController.java b/src/main/java/kw/zeropick/product/controller/ProductSearchController.java new file mode 100644 index 0000000..a25d639 --- /dev/null +++ b/src/main/java/kw/zeropick/product/controller/ProductSearchController.java @@ -0,0 +1,21 @@ +package kw.zeropick.product.controller; + +import kw.zeropick.product.service.ProductSearchService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/product") +@RequiredArgsConstructor +public class ProductSearchController { + private final ProductSearchService productSearchServiceService; + + @PostMapping("/update-all") + public ResponseEntity updateAllProducts() { + productSearchServiceService.updateAllProducts(); + return ResponseEntity.ok("Product information updated successfully."); + } +} diff --git a/src/main/java/kw/zeropick/product/domain/Product.java b/src/main/java/kw/zeropick/product/domain/Product.java index 1e21493..7f24baf 100644 --- a/src/main/java/kw/zeropick/product/domain/Product.java +++ b/src/main/java/kw/zeropick/product/domain/Product.java @@ -45,6 +45,8 @@ public class Product extends BaseEntity { private String imageUrl; + private String productLink; + private int bookmarkCount; private int reviewCount; @@ -112,6 +114,18 @@ public void setPopularity() { this.popularity = this.viewCount * 150 + this.bookmarkCount * 250 + this.reviewCount * 250 + starRateScore; } + public void setProductLink(String productLink) { + this.productLink = productLink; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public void setPrice(int price) { + this.price = price; + } + } diff --git a/src/main/java/kw/zeropick/product/dto/ProductDto.java b/src/main/java/kw/zeropick/product/dto/ProductDto.java index 2a3ed00..427ca02 100644 --- a/src/main/java/kw/zeropick/product/dto/ProductDto.java +++ b/src/main/java/kw/zeropick/product/dto/ProductDto.java @@ -34,6 +34,8 @@ public class ProductDto { private String imageUrl; + private String productLink; // 최저가 상품 링크 추가 + private int bookmarkCount; private int reviewCount; diff --git a/src/main/java/kw/zeropick/product/repository/ProductJpaRepository.java b/src/main/java/kw/zeropick/product/repository/ProductJpaRepository.java index bb2361d..bb6316b 100644 --- a/src/main/java/kw/zeropick/product/repository/ProductJpaRepository.java +++ b/src/main/java/kw/zeropick/product/repository/ProductJpaRepository.java @@ -10,4 +10,5 @@ public interface ProductJpaRepository extends JpaRepository, ProductQueryDslRepository { @Query(value = "SELECT p FROM Product p ORDER BY p.popularity DESC") List findTopProductsByPopularity(); // 모든 상품을 가져오되 인기순으로 정렬 + List findAll(); } diff --git a/src/main/java/kw/zeropick/product/service/ProductSearchService.java b/src/main/java/kw/zeropick/product/service/ProductSearchService.java new file mode 100644 index 0000000..e35f57e --- /dev/null +++ b/src/main/java/kw/zeropick/product/service/ProductSearchService.java @@ -0,0 +1,9 @@ +package kw.zeropick.product.service; + +import kw.zeropick.product.domain.Product; + +public interface ProductSearchService { + public void updateAllProducts(); + public void updateProductInfo(Product product); + +} diff --git a/src/main/java/kw/zeropick/product/service/ProductSearchServiceImpl.java b/src/main/java/kw/zeropick/product/service/ProductSearchServiceImpl.java new file mode 100644 index 0000000..c23c15e --- /dev/null +++ b/src/main/java/kw/zeropick/product/service/ProductSearchServiceImpl.java @@ -0,0 +1,95 @@ +package kw.zeropick.product.service; + +import kw.zeropick.product.domain.Product; +import kw.zeropick.product.repository.ProductJpaRepository; +import lombok.RequiredArgsConstructor; +import org.json.JSONArray; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ProductSearchServiceImpl implements ProductSearchService{ + private final ProductJpaRepository productRepository; + private final RestTemplate restTemplate; + + private static final String NAVER_API_URL = "https://openapi.naver.com/v1/search/shop.json?query="; + @Value("${naver.client.id}") + private String clientId; + + @Value("${naver.client.secret}") + private String clientSecret; + + @Override + @Transactional + public void updateAllProducts() { + List products = productRepository.findAll(); + + for (Product product : products) { + try { + updateProductInfo(product); + Thread.sleep(200); // 0.2초 대기 (초당 5건 제한) + } catch (Exception e) { + System.err.println("Error updating product: " + product.getProductName()); + e.printStackTrace(); + } + } + + } + + @Transactional + public void updateProductInfo(Product product) { + String query = product.getProductName(); + String apiUrl = NAVER_API_URL + query + "&sort=asc"; + + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Naver-Client-Id", clientId); + headers.set("X-Naver-Client-Secret", clientSecret); + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity response = restTemplate.exchange(apiUrl, HttpMethod.GET, entity, String.class); + + if (response.getBody() == null) { + System.err.println("API 응답이 null입니다: " + product.getProductName()); + return; + } + + JSONObject jsonResponse = new JSONObject(response.getBody()); + + if (!jsonResponse.has("items")) { + System.err.println("API 응답에 'items' 필드 없음: " + response.getBody()); + return; + } + + JSONArray items = jsonResponse.getJSONArray("items"); + + if (items.length() == 0) { + System.err.println("검색된 상품 없음: " + product.getProductName()); + return; + } + + JSONObject firstItem = items.getJSONObject(0); // 최저가 상품 + + // 필수 값 검증 + if (!firstItem.has("link") || !firstItem.has("image") || !firstItem.has("lprice")) { + System.err.println("상품 데이터에 필수 필드 없음: " + firstItem.toString()); + return; + } + + product.setProductLink(firstItem.getString("link")); + product.setImageUrl(firstItem.getString("image")); + product.setPrice(Integer.parseInt(firstItem.getString("lprice"))); // 가격이 문자열일 경우 처리 + + productRepository.save(product); + } + +} diff --git a/src/main/java/kw/zeropick/product/service/ProductServiceImpl.java b/src/main/java/kw/zeropick/product/service/ProductServiceImpl.java index 2864508..96c6a1a 100644 --- a/src/main/java/kw/zeropick/product/service/ProductServiceImpl.java +++ b/src/main/java/kw/zeropick/product/service/ProductServiceImpl.java @@ -248,6 +248,7 @@ private ProductDto toProductDto(Product product, Member member) { .starRate(product.getStarRate()) .viewCount(product.getViewCount()) .imageUrl(product.getImageUrl()) + .productLink(product.getProductLink()) .bookmarkCount(product.getBookmarkCount()) .reviewCount(product.getReviewCount()) .bookmarked(isBookmarked)