Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
6ead99e
round1: 1μ£Όμ°¨ λ¬Έμ„œ, 과제 μΆ”κ°€
JVHE Oct 28, 2025
8e06929
round1: redis, kafka μ„€μ • 주석 처리
JVHE Oct 28, 2025
5569bf6
round1: User 객체 κ΅¬ν˜„, ν…ŒμŠ€νŠΈ μž‘μ„± (ID)
JVHE Oct 28, 2025
ee02ce7
round1: User email ν•„λ“œ μΆ”κ°€, email μ œμ•½ ν…ŒμŠ€νŠΈ μž‘μ„±
JVHE Oct 28, 2025
e1d3f05
round1: User birthday μΆ”κ°€, birthday μ œμ•½ ν…ŒμŠ€νŠΈ μž‘μ„±
JVHE Oct 28, 2025
b399fde
round1: User userId, email, birthday null μ•ˆλ˜λ„λ‘ μˆ˜μ •, ν…ŒμŠ€νŠΈ μž‘μ„±
JVHE Oct 28, 2025
a143f91
round1: Userκ°€ BaseEntity 상속받도둝 μˆ˜μ •,User.id -> User.userId둜 μˆ˜μ •
JVHE Oct 29, 2025
0096dae
round1: μœ μ € μ €μž₯ κΈ°λŠ₯ μΆ”κ°€
JVHE Oct 29, 2025
7f05b54
round1: νšŒμ› κ°€μž… API κΈ°λŠ₯ μΆ”κ°€
JVHE Oct 29, 2025
ca1fed1
round1: μœ μ € κ°€μž… μ„±λ³„μš”κ±΄ μΆ”κ°€
JVHE Oct 30, 2025
d5280ea
round1: λ‚΄ 정보 쑰회 κΈ°λŠ₯ μΆ”κ°€
JVHE Oct 30, 2025
05b7b2a
round1: 포인트 쑰회 κΈ°λŠ₯ μΆ”κ°€
JVHE Oct 31, 2025
227d932
round1: 포인트 μΆ©μ „ κΈ°λŠ₯ μΆ”κ°€
JVHE Oct 31, 2025
003b543
docs: 1round.md μΆ”κ°€
adminhelper Oct 27, 2025
00498cd
feat: User 도메인 μΆ”κ°€
adminhelper Oct 27, 2025
1ad67da
test: νšŒμ›κ°€μž… λ‹¨μœ„ ν…ŒμŠ€νŠΈ
adminhelper Oct 27, 2025
4a13366
feat : User 값객체 ν…ŒμŠ€νŠΈ μ™„λ£Œ ν›„ μ‚­μ œ
bookers-web Oct 28, 2025
bc962b2
feat : User 객체 μΆ”κ°€
bookers-web Oct 28, 2025
0605dc4
docs: 1round.md λ¬Έμ„œμˆ˜μ •
adminhelper Oct 28, 2025
f1d94ed
feat: UserRepository, UserService 생성
adminhelper Oct 28, 2025
a51021c
test: νšŒμ› κ°€μž… ν†΅ν•©ν…ŒμŠ€νŠΈ μž‘μ„±
adminhelper Oct 28, 2025
a946ac0
fix: λΆˆν•„μš”ν•œ import μˆ˜μ •
adminhelper Oct 28, 2025
33c0ab8
test: 내정보 쑰회 및 E2E ν…ŒμŠ€νŠΈ
adminhelper Oct 29, 2025
b73c2c6
docs: 1round.md check list update
adminhelper Oct 29, 2025
8d5e90a
feat: 포인트 쑰회 도메인 μΆ”κ°€
adminhelper Oct 29, 2025
d6711cf
feat: 내정보 쑰회 및 E2E ν…ŒμŠ€νŠΈ
adminhelper Oct 29, 2025
7a56684
docs: 1round.md μš”κ΅¬μ‚¬ν•­ μ™„λ£Œ
adminhelper Oct 30, 2025
ded9e38
chore: 파일 μœ„μΉ˜ 이동
adminhelper Oct 30, 2025
5b83c03
feat: 포인트 도메인 μΆ”κ°€
adminhelper Oct 30, 2025
3158c2b
test: 포인트 도메인 λ‹¨μœ„ν…ŒμŠ€νŠΈ
adminhelper Oct 30, 2025
e287600
test: 포인트 도메인 ν†΅ν•©ν…ŒμŠ€νŠΈ
adminhelper Oct 30, 2025
9c5e6ea
test: 포인트 도메인 E2Eν…ŒμŠ€νŠΈ
adminhelper Oct 30, 2025
3774002
refactor: νšŒμ› 도메인 λ¦¬νŒ©ν† λ§
adminhelper Oct 30, 2025
8e5643a
chore: νšŒμ› 도메인 λ¦¬νŒ©ν† λ§
adminhelper Oct 30, 2025
ef3eebd
style: μ½”λ“œ μžλ™μ •λ ¬
bookers-web Oct 31, 2025
d738654
remove: sample code μ‚­μ œ
bookers-web Oct 31, 2025
b06e034
chore: ν…ŒμŠ€νŠΈμ½”λ“œ given when then μž‘μ„±
bookers-web Oct 31, 2025
6c48755
fix: Exception λ©”μ‹œμ§€ μΆ”κ°€
bookers-web Oct 31, 2025
d9ae7ad
Add GitHub Actions workflow for PR Agent
LenKIM Nov 3, 2025
5bb8d33
docs : μœ μ € μ‹œλ‚˜λ¦¬μ˜€ 기반 κΈ°λŠ₯ μ •μ˜, μš”κ΅¬μ‚¬ν•­ λͺ…μ„Έ
adminhelper Nov 7, 2025
d505115
docs : μ‹œν€€μŠ€ λ‹€μ΄μ–΄κ·Έλž¨
adminhelper Nov 7, 2025
624b780
docs : 도메인 객체 섀계 (클래슀 λ‹€μ΄μ–΄κ·Έλž¨)
adminhelper Nov 7, 2025
a97d77b
docs : 전체 ν…Œμ΄λΈ” ꡬ쑰 및 관계 정리
adminhelper Nov 7, 2025
55f8c8b
docs : 2round λ¬Έμ„œ
adminhelper Nov 7, 2025
592a4e5
docs : 1round λ¬Έμ„œ
adminhelper Nov 7, 2025
4faa67e
round1: 리뷰 적용
JVHE Nov 8, 2025
86a8205
round1: 리뷰 적용
JVHE Nov 11, 2025
d06a0d0
round1: 리뷰 적용 - JPA repo κ΅¬ν˜„μ²΄ 뢄리
JVHE Nov 11, 2025
5042c22
docs : 2round λ¬Έμ„œ μˆ˜μ •
adminhelper Nov 14, 2025
4fda674
docs : 3round λ¬Έμ„œμΆ”κ°€
adminhelper Nov 14, 2025
25b423e
feat(brand): λΈŒλžœλ“œ 도메인 λͺ¨λΈ 및 도메인 μ„œλΉ„μŠ€ κ΅¬ν˜„
adminhelper Nov 14, 2025
04ff345
feat(product): μƒν’ˆ 도메인 λͺ¨λΈ κ΅¬ν˜„ 및 재고/μ •λ ¬ κΈ°λŠ₯ μΆ”κ°€
adminhelper Nov 14, 2025
7e0ac82
feat(like): μ’‹μ•„μš” 도메인 κ΅¬ν˜„ 및 λ©±λ“±μ„± 처리 μΆ”κ°€
adminhelper Nov 14, 2025
1087ea2
feat(order, point): μ£Όλ¬Έ 생성 μœ μŠ€μΌ€μ΄μŠ€ 및 포인트 차감 도메인 κ΅¬ν˜„
adminhelper Nov 14, 2025
3e2e0f4
round3: Product, Brand, Like, Order 도메인 κ΅¬ν˜„
JVHE Nov 13, 2025
5db5c36
round3: λΆˆν•„μš”ν•œ μ½”λ“œ 파일 제거
JVHE Nov 14, 2025
aa374c3
round3: μ„€μ • 좩돌 ν•΄κ²°
JVHE Nov 15, 2025
c74e6ad
round3: μ’‹μ•„μš” λ‚΄λ¦Όμ°¨μˆœ μ •λ ¬ 였λ₯˜ μˆ˜μ •
JVHE Nov 15, 2025
c80ed47
round3: λΆˆν•„μš”ν•œ λ¬Έμ„œ 제거
JVHE Nov 15, 2025
c5754ff
fix : PRμ½”λ“œ μˆ˜μ •
adminhelper Nov 17, 2025
deda1e2
Merge branch 'Loopers-dev-lab:main' into 3round
adminhelper Nov 19, 2025
617746d
Merge pull request #86 from adminhelper/3round
adminhelper Nov 19, 2025
be18c88
Revert "[volume-3] 도메인 λͺ¨λΈλ§ 및 κ΅¬ν˜„"
adminhelper Nov 21, 2025
0074ea9
Revert "[volume-3] 도메인 λͺ¨λΈλ§ 및 κ΅¬ν˜„"
adminhelper Nov 21, 2025
b84525a
Revert "Revert "[volume-3] 도메인 λͺ¨λΈλ§ 및 κ΅¬ν˜„""
adminhelper Nov 21, 2025
52b62bd
Merge pull request #110 from Loopers-dev-lab/revert-109-revert-86-3round
adminhelper Nov 21, 2025
34df8d5
Merge pull request #112 from Loopers-dev-lab/revert-86-3round
adminhelper Nov 21, 2025
4ca321e
round3: μ½”λ“œ κ°œμ„ , ν…ŒμŠ€νŠΈ 였λ₯˜ μˆ˜μ •
JVHE Nov 25, 2025
339132b
Merge pull request #95 from JVHE/round3
JVHE Nov 28, 2025
d321b49
Revert "Round3: Product, Brand, Like, Order"
JVHE Nov 28, 2025
caec6fa
Merge pull request #131 from Loopers-dev-lab/revert-95-round3
JVHE Nov 28, 2025
72517a8
feat: μƒν’ˆ 집계 정보 Redis ZSET에 적재
Kimjipang Dec 24, 2025
1cfeaa3
feat: custom exception μ •μ˜
Kimjipang Dec 26, 2025
e102c1d
feat: custom response body μ •μ˜
Kimjipang Dec 26, 2025
cc0c139
chore: 주석 제거
Kimjipang Dec 26, 2025
a75c7ef
feat: μΈκΈ°μƒν’ˆ 쑰회 API κ΅¬ν˜„
Kimjipang Dec 26, 2025
b397ce4
feat: μƒν’ˆ λž­ν‚Ή 정보 쑰회 둜직 κ΅¬ν˜„
Kimjipang Dec 26, 2025
ccc015e
refactor: μƒν’ˆ 상세 정보 API μˆ˜μ •
Kimjipang Dec 26, 2025
dcab5ea
Merge pull request #18 from Kimjipang/round09
Kimjipang Dec 26, 2025
d2586f9
refactor: λž­ν‚Ή 정보 API path μˆ˜μ •
Kimjipang Dec 27, 2025
82284a4
Merge pull request #19 from Kimjipang/round09
Kimjipang Dec 29, 2025
dc745c9
refactor: product_metrics ν…Œμ΄λΈ” 컬럼 μˆ˜μ •
Kimjipang Dec 29, 2025
da195a3
commerce-batch λͺ¨λ“ˆμ„ μΆ”κ°€ν•˜λ©°, 데λͺ¨ Batch Job 및 ν…ŒμŠ€νŠΈλ₯Ό μΆ”κ°€ν•©λ‹ˆλ‹€.
hubtwork Dec 31, 2025
6c87e71
commerce-batch λͺ¨λ“ˆμ— λŒ€ν•΄ README 에 μΆ”κ°€ν•©λ‹ˆλ‹€.
hubtwork Dec 31, 2025
4071e77
Merge remote-tracking branch 'upstream/main'
Kimjipang Jan 1, 2026
acc30d7
Merge pull request #20 from Kimjipang/round10
Kimjipang Jan 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: PR Agent
on:
pull_request:
types: [opened, synchronize]
jobs:
pr_agent_job:
runs-on: ubuntu-latest
steps:
- name: PR Agent action step
uses: Codium-ai/pr-agent@main
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.G_TOKEN }}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ docker-compose -f ./docker/monitoring-compose.yml up
Root
β”œβ”€β”€ apps ( spring-applications )
β”‚ β”œβ”€β”€ πŸ“¦ commerce-api
β”‚ β”œβ”€β”€ πŸ“¦ commerce-batch
β”‚ └── πŸ“¦ commerce-streamer
β”œβ”€β”€ modules ( reusable-configurations )
β”‚ β”œβ”€β”€ πŸ“¦ jpa
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;

@Component
Expand All @@ -25,6 +27,7 @@ public class ProductFacade {
private final ProductRepository productRepository;
private final BrandRepository brandRepository;
private final OutboxRepository outBoxRepository;
private final RankingRedisReader rankingRedisReader;

@Transactional
public ProductInfo registerProduct(ProductV1Dto.ProductRequest request) {
Expand Down Expand Up @@ -52,7 +55,7 @@ public List<ProductInfo> findAllProducts() {

@Transactional
@Cacheable(value = "product", key = "#id")
public ProductInfo findProductById(Long id) {
public ProductRankingInfo findProductById(Long id) {
Product product = productRepository.findById(id).orElseThrow(
() -> new CoreException(ErrorType.NOT_FOUND, "찾고자 ν•˜λŠ” μƒν’ˆμ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")
);
Expand All @@ -65,7 +68,14 @@ public ProductInfo findProductById(Long id) {

outBoxRepository.save(outBoxEvent);

return ProductInfo.from(product);
RankingInfo ranking = null;

try {
String date = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
ranking = rankingRedisReader.getDailyRanking(date, product.getId());
} catch (Exception ignored) {}
Comment on lines +73 to +76
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

μ˜ˆμ™Έλ₯Ό λ¬΄μ‹œν•˜λŠ” catch (Exception ignored) νŒ¨ν„΄ κ°œμ„  ν•„μš”

λͺ¨λ“  μ˜ˆμ™Έλ₯Ό 쑰용히 μ‚Όν‚€λ©΄ Redis μ—°κ²° μ‹€νŒ¨, 직렬화 였λ₯˜ λ“± μ€‘μš”ν•œ 문제λ₯Ό λ””λ²„κΉ…ν•˜κΈ° μ–΄λ ΅μŠ΅λ‹ˆλ‹€. μ΅œμ†Œν•œ λ‘œκΉ…μ„ μΆ”κ°€ν•˜μ„Έμš”.

πŸ”Ž λ‘œκΉ… μΆ”κ°€ μ˜ˆμ‹œ
 try {
     String date = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
     ranking = rankingRedisReader.getDailyRanking(date, product.getId());
-} catch (Exception ignored) {}
+} catch (Exception e) {
+    log.warn("Failed to fetch ranking for product {}: {}", product.getId(), e.getMessage());
+}
πŸ€– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
around lines 73 to 76, replace the silent catch block "catch (Exception ignored)
{}" with an explicit catch that logs the error and relevant context (product id
and date) and only swallows expected recoverable exceptions; e.g., catch
Exception as e -> use the class logger to log at warn/error with a message like
"Failed to read daily ranking for productId=<id> date=<date>" and include the
exception, or narrow to specific exceptions (e.g., Redis or serialization
exceptions) if appropriate, so failures are visible while preserving the
existing fallback behavior.


return ProductRankingInfo.from(product, ranking);
}

@Transactional(readOnly = true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.loopers.application.product;

import com.loopers.domain.product.Product;

import java.math.BigDecimal;

public record ProductRankingInfo(Long id, Long brandId, String name, BigDecimal price, int stock, int likeCount, int rank, double score) {
public static ProductRankingInfo from(Product product, RankingInfo ranking) {
return new ProductRankingInfo(
product.getId(),
product.getBrandId(),
product.getName(),
product.getPrice(),
product.getStock(),
product.getLikeCount(),
ranking.rank(),
ranking.score()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.loopers.application.product;

public record RankingInfo(
String date,
double score,
int rank,
Long total
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.loopers.application.product;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
@RequiredArgsConstructor
public class RankingRedisReader {
private final StringRedisTemplate redisTemplate;

public RankingInfo getDailyRanking(String date, Long productId) {
String key = "ranking:all:" + date;
String member = String.valueOf(productId);

RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
byte[] keyBytes = serializer.serialize(key);
byte[] memberBytes = serializer.serialize(member);

@SuppressWarnings("unchecked")
List<Object> results = (List<Object>) redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
connection.zScore(keyBytes, memberBytes); // -> Double (or null)
connection.zRevRank(keyBytes, memberBytes); // -> Long (or null) 0-base
connection.zCard(keyBytes); // -> Long
return null;
});

Double score = (Double) results.get(0);
Long revRank0 = (Long) results.get(1);
Long total = (Long) results.get(2);

Integer rank = (revRank0 == null) ? null : Math.toIntExact(revRank0 + 1);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Math.toIntExact μ˜€λ²„ν”Œλ‘œμš° κ°€λŠ₯μ„±

revRank0κ°€ Integer.MAX_VALUEλ₯Ό μ΄ˆκ³Όν•˜λ©΄ ArithmeticException이 λ°œμƒν•©λ‹ˆλ‹€. μƒν’ˆ μˆ˜κ°€ λ§Žμ€ ν™˜κ²½μ—μ„œλŠ” μ•ˆμ „ν•œ λ³€ν™˜μ΄ ν•„μš”ν•©λ‹ˆλ‹€.

πŸ”Ž μ•ˆμ „ν•œ λ³€ν™˜ μ œμ•ˆ
-Integer rank = (revRank0 == null) ? null : Math.toIntExact(revRank0 + 1);
+Integer rank = (revRank0 == null) ? null : (int) Math.min(revRank0 + 1, Integer.MAX_VALUE);
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Integer rank = (revRank0 == null) ? null : Math.toIntExact(revRank0 + 1);
Integer rank = (revRank0 == null) ? null : (int) Math.min(revRank0 + 1, Integer.MAX_VALUE);
πŸ€– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/product/RankingRedisReader.java
around line 36, the use of Math.toIntExact(revRank0 + 1) can throw
ArithmeticException if revRank0 + 1 exceeds Integer.MAX_VALUE; replace this with
a safe conversion that first checks for null, computes revRank0 + 1 as a long,
and clamps the result to Integer.MAX_VALUE (or Integer.MIN_VALUE if negative
underflow is possible), then cast to int; also optionally log or metric the
clamping event for observability.


return new RankingInfo(date, score, rank, total);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.loopers.application.ranking;

import com.loopers.interfaces.api.ranking.RankingV1Dto;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

@Component
@RequiredArgsConstructor
public class RankingFacade {
private final StringRedisTemplate redisTemplate;
private static final ZoneId KST = ZoneId.of("Asia/Seoul");
private static final DateTimeFormatter YYYYMMDD = DateTimeFormatter.BASIC_ISO_DATE;

public RankingV1Dto.ProductRankingPageResponse getDailyProductRanking(int page, int size) {
if (page < 1) page = 1;
if (size < 1) size = 20;

String date = LocalDate.now(KST).format(YYYYMMDD);

String key = "ranking:all:" + date;
ZSetOperations<String, String> zset = redisTemplate.opsForZSet();

Long total = zset.size(key);
long totalElements = (total == null) ? 0 : total;

if (totalElements == 0) {
return new RankingV1Dto.ProductRankingPageResponse(date, page, size, 0, 0, List.of());
}

long start = (long) (page - 1) * size;
long end = start + size - 1;

if (start >= totalElements) {
int totalPages = (int) Math.ceil((double) totalElements / size);
return new RankingV1Dto.ProductRankingPageResponse(date, page, size, totalElements, totalPages, List.of());
}

Set<ZSetOperations.TypedTuple<String>> tuples =
zset.reverseRangeWithScores(key, start, end);

List<RankingV1Dto.ProductRankingResponse> items = new ArrayList<>();
if (tuples != null) {
long rank = start + 1;
for (var t : tuples) {
String member = t.getValue();
Double score = t.getScore();
if (member == null || score == null) continue;

items.add(new RankingV1Dto.ProductRankingResponse(
rank++,
Long.parseLong(member),
score
));
Comment on lines +58 to +62
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Long.parseLong μ˜ˆμ™Έ 처리 λˆ„λ½

Redis에 잘λͺ»λœ ν˜•μ‹μ˜ member 데이터가 μ‘΄μž¬ν•  경우 NumberFormatException이 λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€. 방어적 μ½”λ“œλ₯Ό μΆ”κ°€ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€.

πŸ”Ž μ˜ˆμ™Έ 처리 μΆ”κ°€ μ˜ˆμ‹œ
 for (var t : tuples) {
     String member = t.getValue();
     Double score = t.getScore();
     if (member == null || score == null) continue;

+    Long productId;
+    try {
+        productId = Long.parseLong(member);
+    } catch (NumberFormatException e) {
+        continue; // 잘λͺ»λœ λ°μ΄ν„°λŠ” μŠ€ν‚΅
+    }
+
     items.add(new RankingV1Dto.ProductRankingResponse(
             rank++,
-            Long.parseLong(member),
+            productId,
             score
     ));
 }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
items.add(new RankingV1Dto.ProductRankingResponse(
rank++,
Long.parseLong(member),
score
));
for (var t : tuples) {
String member = t.getValue();
Double score = t.getScore();
if (member == null || score == null) continue;
Long productId;
try {
productId = Long.parseLong(member);
} catch (NumberFormatException e) {
continue; // 잘λͺ»λœ λ°μ΄ν„°λŠ” μŠ€ν‚΅
}
items.add(new RankingV1Dto.ProductRankingResponse(
rank++,
productId,
score
));
}
πŸ€– Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/application/RankingFacade.java
around lines 58 to 62, parsing Redis member with Long.parseLong may throw
NumberFormatException for malformed values; wrap the parse in a try-catch that
catches NumberFormatException, log a warning (including the offending member
value and context), and skip that entry (do not add a ProductRankingResponse) so
processing continues for other members; ensure the counter/rank logic remains
consistent when skipping invalid members.

}
}

int totalPages = (int) Math.ceil((double) totalElements / size);
return new RankingV1Dto.ProductRankingPageResponse(date, page, size, totalElements, totalPages, items);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public interface ProductV1ApiSpec {
ApiResponse<List<ProductV1Dto.ProductResponse>> findAllProducts();

@Operation(summary = "μƒν’ˆ 상세 쑰회")
ApiResponse<ProductV1Dto.ProductResponse> findProductById(Long id);
ApiResponse<ProductV1Dto.ProductRankingResponse> findProductById(Long id);

@Operation(summary = "μƒν’ˆ μ •λ ¬ 쑰회")
ApiResponse<List<ProductV1Dto.ProductResponse>> findProductsBySortCondition(ProductV1Dto.SearchProductRequest request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.loopers.application.product.ProductFacade;
import com.loopers.application.product.ProductInfo;
import com.loopers.application.product.ProductRankingInfo;
import com.loopers.interfaces.api.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
Expand Down Expand Up @@ -42,9 +43,9 @@ public ApiResponse<List<ProductV1Dto.ProductResponse>> findAllProducts() {

@GetMapping("/{id}")
@Override
public ApiResponse<ProductV1Dto.ProductResponse> findProductById(@PathVariable Long id) {
ProductInfo info = productFacade.findProductById(id);
ProductV1Dto.ProductResponse response = ProductV1Dto.ProductResponse.from(info);
public ApiResponse<ProductV1Dto.ProductRankingResponse> findProductById(@PathVariable Long id) {
ProductRankingInfo info = productFacade.findProductById(id);
ProductV1Dto.ProductRankingResponse response = ProductV1Dto.ProductRankingResponse.from(info);

return ApiResponse.success(response);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
package com.loopers.interfaces.api.product;

import com.loopers.application.product.ProductInfo;
import com.loopers.application.product.ProductRankingInfo;
import com.loopers.domain.product.Product;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;

import java.math.BigDecimal;

public class ProductV1Dto {
public record ProductRankingResponse(Long id, Long brandId, String name, BigDecimal price, int stock, int rank, double score) {
public static ProductRankingResponse from(ProductRankingInfo info) {
return new ProductRankingResponse(
info.id(),
info.brandId(),
info.name(),
info.price(),
info.stock(),
info.rank(),
info.score()
);
}
}
public record ProductResponse(Long id, Long brandId, String name, BigDecimal price, int stock, int likeCount) {
public static ProductResponse from(ProductInfo info) {
return new ProductResponse(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.loopers.interfaces.api.ranking;

import com.loopers.interfaces.api.ApiResponse;

public interface RankingV1ApiSpec {

ApiResponse<RankingV1Dto.ProductRankingPageResponse> getDailyProductRanking(int size, int page);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.loopers.interfaces.api.ranking;

import com.loopers.application.ranking.RankingFacade;
import com.loopers.interfaces.api.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/rankings")
@RequiredArgsConstructor
public class RankingV1Controller implements RankingV1ApiSpec {
private final RankingFacade rankingFacade;

@GetMapping
@Override
public ApiResponse<RankingV1Dto.ProductRankingPageResponse> getDailyProductRanking(
@RequestParam int size,
@RequestParam int page
) {
RankingV1Dto.ProductRankingPageResponse response = rankingFacade.getDailyProductRanking(page, size);

return ApiResponse.success(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.loopers.interfaces.api.ranking;

import java.util.List;

public class RankingV1Dto {
public record ProductRankingResponse(
Long rank,
Long productId,
double score
) {}

public record ProductRankingPageResponse(
String date,
int page,
int size,
long totalElements,
int totalPages,
List<ProductRankingResponse> items
) {}
}
21 changes: 21 additions & 0 deletions apps/commerce-batch/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
dependencies {
// add-ons
implementation(project(":modules:jpa"))
implementation(project(":modules:redis"))
implementation(project(":supports:jackson"))
implementation(project(":supports:logging"))
implementation(project(":supports:monitoring"))

// batch
implementation("org.springframework.boot:spring-boot-starter-batch")
testImplementation("org.springframework.batch:spring-batch-test")

// querydsl
annotationProcessor("com.querydsl:querydsl-apt::jakarta")
annotationProcessor("jakarta.persistence:jakarta.persistence-api")
annotationProcessor("jakarta.annotation:jakarta.annotation-api")

// test-fixtures
testImplementation(testFixtures(project(":modules:jpa")))
testImplementation(testFixtures(project(":modules:redis")))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.loopers;

import jakarta.annotation.PostConstruct;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

import java.util.TimeZone;

@ConfigurationPropertiesScan
@SpringBootApplication
public class CommerceBatchApplication {

@PostConstruct
public void started() {
// set timezone
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
}

public static void main(String[] args) {
int exitCode = SpringApplication.exit(SpringApplication.run(CommerceBatchApplication.class, args));
System.exit(exitCode);
}
}
Loading