-
Notifications
You must be signed in to change notification settings - Fork 34
[volume-9] Product Ranking with Redis #224
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: jeonga1022
Are you sure you want to change the base?
Changes from all commits
61b0df6
77225a4
bca67df
7275f5b
bc18cc4
d47efeb
e38d21b
3cf0dd6
dc137cf
82bd67b
e4a6149
569b5ae
045e6cb
c5a89f6
f994a91
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| package com.loopers.application.ranking; | ||
|
|
||
| import com.loopers.domain.product.Product; | ||
| import com.loopers.domain.product.ProductRepository; | ||
| import com.loopers.infrastructure.ranking.RankingEntry; | ||
| import com.loopers.infrastructure.ranking.RankingRedisService; | ||
| import com.loopers.interfaces.api.ranking.RankingDto; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class RankingFacade { | ||
|
|
||
| private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); | ||
|
|
||
| private final RankingRedisService rankingRedisService; | ||
| private final ProductRepository productRepository; | ||
|
|
||
| public RankingDto.RankingListResponse getRankings(String dateStr, int page, int size) { | ||
| LocalDate date = parseDate(dateStr); | ||
| int offset = page * size; | ||
|
|
||
| List<RankingEntry> entries = rankingRedisService.getTopProducts(date, offset, size); | ||
|
|
||
| if (entries.isEmpty()) { | ||
| return new RankingDto.RankingListResponse(List.of(), page, size, 0); | ||
| } | ||
|
|
||
| long totalCount = rankingRedisService.getTotalCount(date); | ||
|
|
||
| List<Long> productIds = entries.stream() | ||
| .map(RankingEntry::productId) | ||
| .toList(); | ||
|
|
||
| Map<Long, Product> productMap = productRepository.findAllByIdIn(productIds).stream() | ||
| .collect(Collectors.toMap(Product::getId, p -> p)); | ||
|
|
||
| List<RankingDto.RankingResponse> rankings = new ArrayList<>(); | ||
| int rank = offset + 1; | ||
| for (RankingEntry entry : entries) { | ||
| Product product = productMap.get(entry.productId()); | ||
| if (product != null) { | ||
| rankings.add(new RankingDto.RankingResponse( | ||
| rank++, | ||
| product.getId(), | ||
| product.getName(), | ||
| product.getPrice(), | ||
| entry.score() | ||
| )); | ||
| } | ||
| } | ||
|
|
||
| return new RankingDto.RankingListResponse(rankings, page, size, totalCount); | ||
| } | ||
|
|
||
| private LocalDate parseDate(String dateStr) { | ||
| if (dateStr == null || dateStr.isBlank()) { | ||
| return LocalDate.now(); | ||
| } | ||
| return LocalDate.parse(dateStr, DATE_FORMATTER); | ||
| } | ||
|
Comment on lines
+64
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ์๋ชป๋ ๋ ์ง ํ์์ ๋ํ ์์ธ ์ฒ๋ฆฌ ํ์
๐ ์ ์ ์์ private LocalDate parseDate(String dateStr) {
if (dateStr == null || dateStr.isBlank()) {
return LocalDate.now();
}
- return LocalDate.parse(dateStr, DATE_FORMATTER);
+ try {
+ return LocalDate.parse(dateStr, DATE_FORMATTER);
+ } catch (DateTimeParseException e) {
+ throw new IllegalArgumentException("์๋ชป๋ ๋ ์ง ํ์์
๋๋ค. yyyyMMdd ํ์์ ์ฌ์ฉํด์ฃผ์ธ์: " + dateStr, e);
+ }
}๐ค Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package com.loopers.domain.product; | ||
|
|
||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.GenerationType; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.Index; | ||
| import jakarta.persistence.PrePersist; | ||
| import jakarta.persistence.Table; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Entity | ||
| @Table(name = "product_view_logs", indexes = { | ||
| @Index(name = "idx_product_id_created_at", columnList = "product_id, created_at") | ||
| }) | ||
| public class ProductViewLog { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @Column(name = "product_id", nullable = false) | ||
| private Long productId; | ||
|
|
||
| @Column(name = "created_at", nullable = false, updatable = false) | ||
| private LocalDateTime createdAt; | ||
|
|
||
| protected ProductViewLog() { | ||
| } | ||
|
|
||
| private ProductViewLog(Long productId) { | ||
| this.productId = productId; | ||
| } | ||
|
|
||
| public static ProductViewLog create(Long productId) { | ||
| return new ProductViewLog(productId); | ||
| } | ||
|
|
||
| @PrePersist | ||
| private void prePersist() { | ||
| this.createdAt = LocalDateTime.now(); | ||
| } | ||
|
|
||
| public Long getId() { | ||
| return id; | ||
| } | ||
|
|
||
| public Long getProductId() { | ||
| return productId; | ||
| } | ||
|
|
||
| public LocalDateTime getCreatedAt() { | ||
| return createdAt; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.loopers.domain.product; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public interface ProductViewLogRepository { | ||
|
|
||
| List<ProductViewLog> saveAll(List<ProductViewLog> logs); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package com.loopers.domain.product.event; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| public class ProductViewedEvent { | ||
|
|
||
| private Long productId; | ||
| private LocalDateTime occurredAt; | ||
|
|
||
| protected ProductViewedEvent() { | ||
| } | ||
|
|
||
| private ProductViewedEvent(Long productId) { | ||
| this.productId = productId; | ||
| this.occurredAt = LocalDateTime.now(); | ||
| } | ||
|
|
||
| public static ProductViewedEvent of(Long productId) { | ||
| return new ProductViewedEvent(productId); | ||
| } | ||
|
|
||
| public Long getProductId() { | ||
| return productId; | ||
| } | ||
|
|
||
| public LocalDateTime getOccurredAt() { | ||
| return occurredAt; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐งฉ Analysis chain
๐ Script executed:
Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 279
๐ Script executed:
Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 10577
๐ Script executed:
Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 174
๐ Script executed:
Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1600
๐ Script executed:
Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1426
๐ Script executed:
Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 70
Redis ์ฅ์ ์ ์๋ฌ ํธ๋ค๋ง์ด ์์ต๋๋ค.
getRankingPositionํธ์ถ์ด try-catch๋ก ๊ฐ์ธ์ ธ ์์ง ์์ผ๋ฉฐ, Redis ์ฐ๊ฒฐ ์คํจ ์ ์์ธ๊ฐ ๊ทธ๋๋ก ์ ํ๋์ด ์ํ ์์ธ ์กฐํ API ์ ์ฒด๊ฐ ์คํจํฉ๋๋ค. rank ๊ฐ์ด null์ผ ์๋ ์์ง๋ง(์ํ์ด ์์์ ์๋ ๊ฒฝ์ฐ), Redis ์์ธ์ ๋ํ ์ฒ๋ฆฌ๊ฐ ํ์ํฉ๋๋ค. ๋ค์ ์ค ํ๋์ ๋ฐฉ์์ผ๋ก graceful degradation์ ์ ์ฉํด ์ฃผ์ธ์:getRankingPositionํธ์ถ์ try-catch๋ก ๊ฐ์ธ๊ณ ์์ธ ๋ฐ์ ์ null ๋ฐํRankingRedisService์์ Redis ์์ธ๋ฅผ ์ฒ๋ฆฌํ๊ณ ์์ ํ๊ฒ null ๋ฐํ๐ค Prompt for AI Agents