Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package com.loopers.application.like;

import com.loopers.application.like.event.LikeCanceledEvent;
import com.loopers.application.like.event.LikeCreatedEvent;
import com.loopers.domain.like.Like;
import com.loopers.domain.like.LikeService;
import com.loopers.domain.product.Product;
import com.loopers.domain.product.ProductLikeCountService;
import com.loopers.domain.product.ProductService;
import com.loopers.messaging.event.LikeCanceledEvent;
import com.loopers.messaging.event.LikeCreatedEvent;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import com.loopers.domain.common.event.DomainEventEnvelop;
import com.loopers.domain.common.event.DomainEventRepository;
import com.loopers.domain.product.ProductLikeCountService;
import com.loopers.messaging.event.LikeCanceledEvent;
import com.loopers.messaging.event.LikeCreatedEvent;
import com.loopers.support.json.JsonConverter;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Async;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import com.loopers.application.order.OrderExternalSystemSender;
import com.loopers.application.order.OrderFacade;
import com.loopers.application.payment.event.PaymentFailedEvent;
import com.loopers.application.payment.event.PaymentSucceededEvent;
import com.loopers.domain.common.event.DomainEvent;
import com.loopers.domain.common.event.DomainEventEnvelop;
import com.loopers.domain.common.event.DomainEventRepository;
import com.loopers.messaging.event.OrderPaidEvent;
import com.loopers.support.json.JsonConverter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -31,8 +31,8 @@ public class OrderEventHandler {

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handle(PaymentSucceededEvent event) {
log.info("๐Ÿ”ฅ PaymentSucceededEvent handler ์ง„์ž…");
public void handle(OrderPaidEvent event) {
log.info("๐Ÿ”ฅ OrderPaidEvent handler ์ง„์ž…");
orderFacade.handleOrderSucceed(event.orderId());
}

Expand All @@ -48,7 +48,7 @@ public void handle(PaymentFailedEvent event) {
*/
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Async
void handleOrderCreatedExternalSend(PaymentSucceededEvent event) {
void handleOrderCreatedExternalSend(OrderPaidEvent event) {
try {
orderExternalSystemSender.send(event.orderId());
} catch (Exception e) {
Expand All @@ -61,9 +61,9 @@ void handleOrderCreatedExternalSend(PaymentSucceededEvent event) {
* ๊ฒฐ์ œ ์„ฑ๊ณต ์‹œ, outbox ํ…Œ์ด๋ธ”์— ์ด๋ฒคํŠธ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
*/
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void handleOutboxEvent(PaymentSucceededEvent event) {
public void handleOutboxEvent(OrderPaidEvent event) {

DomainEventEnvelop<PaymentSucceededEvent> envelop =
DomainEventEnvelop<OrderPaidEvent> envelop =
DomainEventEnvelop.of(
"ORDER_PAID",
"ORDER",
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package com.loopers.application.payment;

import com.loopers.application.order.event.OrderCreated;
import com.loopers.application.payment.event.PaymentSucceededEvent;
import com.loopers.domain.order.Order;
import com.loopers.domain.order.OrderService;
import com.loopers.domain.payment.Payment;
import com.loopers.domain.payment.PaymentService;
import com.loopers.domain.point.PointService;
import com.loopers.infrastructure.ResilientPgClient;
import com.loopers.interfaces.api.payment.PaymentV1Dto;
import com.loopers.messaging.event.OrderPaidEvent;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import lombok.RequiredArgsConstructor;
Expand All @@ -17,6 +17,8 @@
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Slf4j
@Component
@RequiredArgsConstructor
Expand Down Expand Up @@ -51,7 +53,16 @@ public void pay(Long userId, Long orderId) {
case POINT -> {
try {
pointService.usePoint(payment.getUserId(), payment.getAmount());
eventPublisher.publishEvent(PaymentSucceededEvent.from(orderId));

List<OrderPaidEvent.OrderItemData> items =
order.getItems().stream()
.map(item -> new OrderPaidEvent.OrderItemData(
item.getProductId(),
item.getQuantity(),
item.getTotalPrice().getAmount()
)).toList();
Comment on lines +57 to +63
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

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

# Find OrderItem class and price-related methods
find . -name "*.java" -path "*/domain/order/*" | head -20

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 876


๐Ÿ Script executed:

# Search for OrderItem class definition
rg -l "class OrderItem" --type=java

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 235


๐Ÿ Script executed:

# Look for getTotalPrice and getUnitPrice methods in order-related classes
rg -n "getTotalPrice|getUnitPrice|getPrice\(" --type=java apps/commerce-api/src/main/java/com/loopers/domain/order/ -A 3 -B 1

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 597


๐Ÿ Script executed:

# Search for OrderItemData class definition
rg -n "class OrderItemData" --type=java -A 10

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 70


๐Ÿ Script executed:

# Search for OrderItemData anywhere in the codebase
rg -n "OrderItemData" --type=java -B 2 -A 5

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 6069


๐Ÿ Script executed:

# Read the OrderItem class to understand its structure
cat -n apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 2326


๐Ÿ Script executed:

# Search for all references to unitPrice or totalPrice in OrderItem-related files
rg -n "unitPrice|totalPrice" --type=java apps/commerce-api/src/main/java/com/loopers/domain/order/

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 1770


getTotalPrice() ๋Œ€์‹  getUnitPrice()๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

OrderItemData์˜ unitPrice ํ•„๋“œ๋Š” ์ƒํ’ˆ ๋‹จ๊ฐ€๋ฅผ ์˜๋„ํ•˜๊ณ  ์žˆ์œผ๋‚˜, ํ˜„์žฌ ์ฝ”๋“œ๋Š” item.getTotalPrice().getAmount()๋ฅผ ์ „๋‹ฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. OrderItem์˜ totalPrice๋Š” ์ˆ˜๋Ÿ‰ ร— ๋‹จ๊ฐ€์ด๋ฏ€๋กœ, ๋‹ค์šด์ŠคํŠธ๋ฆผ ์ปจ์Šˆ๋จธ์—์„œ ์ž˜๋ชป๋œ ๊ณ„์‚ฐ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฐ™์€ ๋กœ์ง์ด PaymentProcessService์—์„œ๋Š” item.getUnitPrice().getAmount()๋กœ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด 62๋ฒˆ ์ค„์„ item.getUnitPrice().getAmount()๋กœ ๋ณ€๊ฒฝํ•˜์„ธ์š”.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentFacade.java
around lines 57 to 63, the code passes item.getTotalPrice().getAmount() into
OrderItemData.unitPrice which should be the per-item unit price; replace that
call with item.getUnitPrice().getAmount() so the unitPrice field receives the
single-item price (matching PaymentProcessService) and avoid downstream
miscalculations.


eventPublisher.publishEvent(OrderPaidEvent.of(orderId, items));
} catch (IllegalArgumentException e) {
throw new CoreException(ErrorType.BAD_REQUEST, e.getMessage());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
package com.loopers.application.payment;

import com.loopers.application.payment.event.PaymentFailedEvent;
import com.loopers.application.payment.event.PaymentSucceededEvent;
import com.loopers.domain.order.Order;
import com.loopers.domain.order.OrderService;
import com.loopers.messaging.event.OrderPaidEvent;
import com.loopers.messaging.event.OrderPaidEvent.OrderItemData;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@Component
@RequiredArgsConstructor
public class PaymentProcessService {

private final PaymentFacade paymentFacade;
private final OrderService orderService; // Added OrderService injection

private final ApplicationEventPublisher eventPublisher;

Expand All @@ -28,12 +35,19 @@ public void process(Long userId, Long orderId) {
}

@Transactional
public void processPg(Long userId, Long orderId) {
public void processPg(Long userId, Long orderId) { // Added userId parameter
try {
paymentFacade.payPg(orderId);
eventPublisher.publishEvent(PaymentSucceededEvent.from(orderId));

Order order = orderService.findOrderById(orderId)
.orElseThrow(() -> new IllegalStateException("๊ฒฐ์ œ๋œ ์ฃผ๋ฌธ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. orderId: " + orderId));

List<OrderItemData> orderItemDataList = order.getItems().stream()
.map(item -> new OrderItemData(item.getProductId(), item.getQuantity(), item.getUnitPrice().getAmount()))
.collect(Collectors.toList());

eventPublisher.publishEvent(OrderPaidEvent.of(orderId, orderItemDataList));
Comment on lines +42 to +49
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐Ÿ”ด Critical

์ค‘์š”: OrderItemData ์ƒ์„ฑ ์‹œ totalPrice๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Line 46์—์„œ item.getUnitPrice().getAmount()๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์ง€๋งŒ, PaymentFacade.java(line 44)์—์„œ๋Š” ๋™์ผํ•œ ๋ชฉ์ ์œผ๋กœ item.getTotalPrice().getAmount()๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. OrderItemData์˜ ์„ธ ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ํ•ด๋‹น ์ฃผ๋ฌธ ํ•ญ๋ชฉ์˜ ์ด ๊ธˆ์•ก(์ˆ˜๋Ÿ‰ ร— ๋‹จ๊ฐ€)์„ ๋‚˜ํƒ€๋‚ด์•ผ ํ•˜๋ฏ€๋กœ, unitPrice ๋Œ€์‹  totalPrice๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ด ๋ถˆ์ผ์น˜๋กœ ์ธํ•ด:

  • ํŒ๋งค ๋ฐ์ดํ„ฐ๊ฐ€ ์‹ค์ œ๋ณด๋‹ค ์ ๊ฒŒ ์ง‘๊ณ„๋ฉ๋‹ˆ๋‹ค
  • POINT ๊ฒฐ์ œ(PaymentFacade)์™€ PG ๊ฒฐ์ œ(PaymentProcessService) ๊ฐ„ ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค
  • ์ƒํ’ˆ ๋žญํ‚น ์ง‘๊ณ„์— ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค
๐Ÿ”Ž ์ˆ˜์ • ์ œ์•ˆ
             List<OrderItemData> orderItemDataList = order.getItems().stream()
-                    .map(item -> new OrderItemData(item.getProductId(), item.getQuantity(), item.getUnitPrice().getAmount()))
+                    .map(item -> new OrderItemData(item.getProductId(), item.getQuantity(), item.getTotalPrice().getAmount()))
                     .collect(Collectors.toList());
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentProcessService.java
around lines 42 to 49, OrderItemData is being constructed with
item.getUnitPrice().getAmount() but must use the item's total price; replace the
third constructor argument to use item.getTotalPrice().getAmount() so the
OrderItemData reflects quantity ร— unit price (matching PaymentFacade) and then
publish the event with the corrected list.

} catch (Exception e) {
// ์ด์™ธ ์„œ๋ฒ„ ํƒ€์ž„์•„์›ƒ ๋“ฑ์€ retry -> pending์ƒํƒœ๋กœ ์Šค์ผ€์ค„๋ง ์‹œ๋„
log.error("์™ธ๋ถ€ PG ๊ฒฐ์ œ ์‹คํŒจ, ์ฃผ๋ฌธ ID: {}", orderId, e);
eventPublisher.publishEvent(PaymentFailedEvent.of(userId, orderId, e));
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@ public record ProductDetailInfo(
Long brandId,
String brandName,
BigDecimal priceAmount,
int likeCount
int likeCount,
Long rank
) {
public static ProductDetailInfo from(ProductDetailProjection p) {
public static ProductDetailInfo of(ProductDetailProjection p, Long rank) {
return new ProductDetailInfo(
p.getProductId(),
p.getProductName(),
p.getBrandId(),
p.getBrandName(),
p.getPrice().getAmount(),
p.getLikeCount()
p.getLikeCount(),
rank
);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package com.loopers.application.product;

import com.fasterxml.jackson.core.type.TypeReference;
import com.loopers.application.product.event.ProductViewedEvent;
import com.loopers.cache.CacheKeyService;
import com.loopers.cache.CacheService;
import com.loopers.domain.product.ProductService;
import com.loopers.domain.ranking.RankingService;
import com.loopers.messaging.event.ProductViewedEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
Expand All @@ -14,6 +15,7 @@
import org.springframework.transaction.annotation.Transactional;

import java.time.Duration;
import java.time.LocalDate;

@RequiredArgsConstructor
@Component
Expand All @@ -26,6 +28,7 @@ public class ProductFacade {
private final ProductQueryService productQueryService;
private final ApplicationEventPublisher eventPublisher;

private final RankingService rankingService;

private static final Duration TTL_LIST = Duration.ofMinutes(10);
private static final Duration TTL_DETAIL = Duration.ofMinutes(5);
Expand Down Expand Up @@ -55,9 +58,11 @@ private Page<ProductInfo> loadProducts(Long brandId, Pageable pageable, String s
public ProductDetailInfo getProductDetail(Long productId) {
String key = "product:v1:detail:" + productId;

Long rank = rankingService.getRanking(LocalDate.now(), productId);

ProductDetailInfo productDetailInfo = cacheService.getOrLoad(
key,
() -> productService.getProductDetail(productId),
() -> ProductDetailInfo.of(productService.getProductDetail(productId), rank),
TTL_DETAIL,
ProductDetailInfo.class);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.loopers.application.product.event;

import com.loopers.domain.common.event.DomainEvent;
import com.loopers.domain.common.event.DomainEventEnvelop;
import com.loopers.domain.common.event.DomainEventRepository;
import com.loopers.messaging.event.ProductViewedEvent;
import com.loopers.support.json.JsonConverter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Slf4j
@Component
@RequiredArgsConstructor
public class ProductViewedEventHandler {

private final DomainEventRepository eventRepository;
private final JsonConverter jsonConverter;

@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void handleProductViewedEvent(ProductViewedEvent event) {
log.info("ProductViewedEvent received: productId={}, occurredAt={}", event.productId(), event.occurredAt());

DomainEventEnvelop<ProductViewedEvent> envelop =
DomainEventEnvelop.of(
"PRODUCT_VIEWED",
"PRODUCT",
event.productId(),
event
);

eventRepository.save(
DomainEvent.pending(
"catalog-events",
envelop,
jsonConverter.serialize(envelop)
)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.loopers.application.ranking;

import com.loopers.domain.product.Product;
import com.loopers.domain.product.ProductLikeCountService;
import com.loopers.domain.product.ProductService;
import com.loopers.domain.ranking.RankingService;
import com.loopers.interfaces.api.ranking.RankingV1Dto.RankingProductResponse;
import com.loopers.ranking.streamer.RankingInfo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class RankingFacade {
private final RankingService rankingService;
private final ProductService productService;
private final ProductLikeCountService productLikeCountService;

public Page<RankingProductResponse> getRankings(LocalDate rankingDate, Pageable pageable) {

// 1. Redis ZSET์—์„œ ๋žญํ‚น ์ƒํ’ˆ ID์™€ ์ ์ˆ˜๋ฅผ ์กฐํšŒ
List<RankingInfo> rankedProducts = rankingService.getRankings(rankingDate, pageable);
long totalCount = rankingService.getTotalCount(rankingDate);

// 2. ๋žญํ‚น ์ƒํ’ˆ ID๋“ค์„ ์ถ”์ถœ
List<Long> productIds = rankedProducts.stream()
.map(RankingInfo::id)
.toList();

// 3. ProductService ํ†ตํ•ด ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ (์ƒํ’ˆ ID ์ˆœ์„œ ์œ ์ง€๋ฅผ ์œ„ํ•ด Map ์‚ฌ์šฉ)
Map<Long, Product> productInfos = productService.getProductsMapByIds(productIds);

// 4. ์กฐํšŒ๋œ ์ƒํ’ˆ ์ •๋ณด์™€ ๋žญํ‚น ๋ฐ์ดํ„ฐ๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ ์‘๋‹ต DTO ์ƒ์„ฑ
List<RankingProductResponse> rankingResponses = rankedProducts.stream()
.map(ranking -> {
Product product = productInfos.get(ranking.id());

if (product == null) {
log.warn("์ƒํ’ˆ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ƒํ’ˆID : {}", ranking.id());
return null;
}

// ์ข‹์•„์š” ์ˆ˜๋ฅผ ProductLikeCountService์—์„œ ์กฐํšŒ (์—†์œผ๋ฉด 0)
int likeCount = productLikeCountService.findById(product.getId())
.map(pc -> pc.getLikeCount())
.orElse(0);

return new RankingProductResponse(
product.getId(),
product.getName(),
product.getBrandId(),
product.getPrice().getAmount(),
likeCount,
ranking.score(),
ranking.rank(),
LocalDateTime.now() // createdAt?
);
})
.filter(Objects::nonNull)
.collect(Collectors.toList());

Page<RankingProductResponse> responsePage = new PageImpl<>(
rankingResponses,
pageable,
totalCount
);

return responsePage;
};
}
Loading