Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6a03748
feat: DIPμœ„λ°˜ ν”Όλ“œλ°± 반영
adminhelper Dec 4, 2025
c41ffe6
chore: BaseEntityμƒμ†μœΌλ‘œ μΈν•œ createdAt제거
adminhelper Dec 4, 2025
afcae1c
chore: ν”Όλ“œλ°± 반영
adminhelper Dec 4, 2025
df0fb03
docs : 6round.md
adminhelper Dec 4, 2025
7b1111b
docs: md파일 μˆ˜μ •
adminhelper Dec 5, 2025
5e36c3f
feat : pg-simulator λͺ¨λ“ˆ μΆ”κ°€
adminhelper Dec 5, 2025
8fb6e51
feat : pg-simulator λͺ¨λ“ˆ μΆ”κ°€
adminhelper Dec 5, 2025
23b9170
feat : 주문 API 적용
adminhelper Dec 5, 2025
a1ffe5e
Refactor: DIPμœ„λ°˜ 제거
adminhelper Dec 5, 2025
f1205ab
fix: Point ν™˜λΆˆ λ©”μ„œλ“œ μΆ”κ°€
adminhelper Dec 5, 2025
c5f6eaf
feat: resilience4j μ˜μ‘΄μ„± μΆ”κ°€
adminhelper Dec 5, 2025
8c6d050
docs: 6round.md
adminhelper Dec 6, 2025
0e3419f
feat: resilience4j, feign μ˜μ‘΄μ„± μΆ”κ°€
adminhelper Dec 6, 2025
20d6942
feat: pg, feign, resilience4j μ„€μ •
adminhelper Dec 6, 2025
7d12217
fix: user μƒμ„±μ‹œ point 생성 둜직 μˆ˜μ •
adminhelper Dec 6, 2025
38838b4
feat: PG 결제 μš”μ²­ API 둜직 μΆ”κ°€
adminhelper Dec 6, 2025
ff83042
fix: 6round ν”Όλ“œλ°±
adminhelper Dec 8, 2025
482bed2
docs: 7round.md
adminhelper Dec 8, 2025
859eefc
docs: 7round.md
adminhelper Dec 12, 2025
743262e
feat: 이벀트기반으둜 μ’‹μ•„μš” 처리
adminhelper Dec 12, 2025
11bcdfb
feat: 주문결제 λΆ„λ¦¬μ œ κ±° 및 이벀트기반으둜 처리
adminhelper Dec 12, 2025
dd8cb1a
feat: 데이터 ν”Œλž«νΌ μ „μ†Œν•˜λŠ” ν›„μ†μ²˜λ¦¬
adminhelper Dec 12, 2025
cf6c0c5
docs: 8round.md μΆ”κ°€
adminhelper Dec 19, 2025
ef5fd9d
docs: 8round.md μˆ˜μ •
adminhelper Dec 25, 2025
94b5739
feat: kafka μ„€μ •λ³€κ²½
adminhelper Dec 25, 2025
4ab9eba
feat: outbox νŒ¨ν„΄ μΆ”κ°€
adminhelper Dec 25, 2025
96bcccd
feat: 도메인 이벀트 λ°œμƒ topic μΆ”κ°€
adminhelper Dec 25, 2025
5fa1a90
chore: νŠΈλžœμž­μ…˜ μ „νŒŒμ˜΅μ…˜ 제거
adminhelper Dec 25, 2025
548647b
feat: event_handled ν…Œμ΄λΈ”ν†΅ν•œ λ©±λ“±μ²˜λ¦¬
adminhelper Dec 25, 2025
e0d5737
feat: Metrics μ§‘κ³„μ²˜λ¦¬
adminhelper Dec 25, 2025
987d68a
feat: μž¬κ³ μ†Œμ§„μ‹œ μƒν’ˆμΊμ‹œ κ°±μ‹ 
adminhelper Dec 25, 2025
171f573
docs: 9round.md
adminhelper Dec 26, 2025
c325c4d
feat: Ranking API κ΅¬ν˜„
adminhelper Dec 26, 2025
982af43
feat: μƒν’ˆ 상세 μ‘°νšŒμ‹œ μƒν’ˆ μˆœμœ„ ν•¨κ»˜ λ°˜ν™˜
adminhelper Dec 26, 2025
011682b
feat: Zset, TTL, key μ •μ±… 적용
adminhelper Dec 26, 2025
7bdcac5
feat: μ½œλ“œ μŠ€νƒ€νŠΈ μŠ€μΌ€μ€„λŸ¬ κ΅¬ν˜„
adminhelper Dec 26, 2025
31afd01
feat: catalog-events, order-events μ΄λ²€νŠΈμΆ”κ°€
adminhelper Dec 26, 2025
1bc3858
refactor: 9round λ¦¬νŒ©ν† λ§
adminhelper Dec 30, 2025
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
7 changes: 7 additions & 0 deletions apps/commerce-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ dependencies {
// add-ons
implementation(project(":modules:jpa"))
implementation(project(":modules:redis"))
implementation(project(":modules:kafka"))
implementation(project(":supports:jackson"))
implementation(project(":supports:logging"))
implementation(project(":supports:monitoring"))
Expand All @@ -11,6 +12,12 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:${project.properties["springDocOpenApiVersion"]}")

// resilience4j
implementation("io.github.resilience4j:resilience4j-spring-boot3")

// feign
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")

// querydsl
annotationProcessor("com.querydsl:querydsl-apt::jakarta")
annotationProcessor("jakarta.persistence:jakarta.persistence-api")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

import java.util.TimeZone;

@ConfigurationPropertiesScan
@EnableFeignClients
@EnableScheduling
@EnableAsync
@SpringBootApplication
public class CommerceApiApplication {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@ public class LikeFacade {

private final LikeService likeService;

public void createLike(String userId, Long productId) {
public void like(String userId, Long productId) {
likeService.like(userId, productId);
}

public void deleteLike(String userId, Long productId) {
public void unlike(String userId, Long productId) {
likeService.unlike(userId, productId);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
*/
public record CreateOrderCommand(
String userId,
List<OrderItemCommand> items
List<OrderItemCommand> items,
OrderPaymentCommand payment
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,33 @@
import com.loopers.domain.order.Order;
import com.loopers.domain.order.OrderItem;
import com.loopers.domain.order.OrderService;
import com.loopers.domain.order.OrderStatus;
import com.loopers.domain.order.event.OrderEvent;
import com.loopers.domain.order.event.OrderEventPublisher;
import com.loopers.domain.payment.Payment;
import com.loopers.domain.payment.PaymentService;
import com.loopers.domain.point.Point;
import com.loopers.domain.point.PointService;
import com.loopers.domain.product.Product;
import com.loopers.domain.product.ProductService;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

/**
* packageName : com.loopers.application.order
* fileName : OrderFacade
* author : byeonsungmun
* date : 2025. 11. 13.
* description :
* ===========================================
* DATE AUTHOR NOTE
* -------------------------------------------
* 2025. 11. 13. byeonsungmun 졜초 생성
*/

@Slf4j
@Component
@RequiredArgsConstructor
public class OrderFacade {

private final OrderService orderService;
private final ProductService productService;
private final PointService pointService;
private final PaymentService paymentService;
private final OrderEventPublisher orderEventPublisher;
@Value("${app.callback.base-url}")
private String callbackBaseUrl;


@Transactional
public OrderInfo createOrder(CreateOrderCommand command) {
Expand All @@ -47,13 +42,10 @@ public OrderInfo createOrder(CreateOrderCommand command) {

for (OrderItemCommand itemCommand : command.items()) {

//μƒν’ˆκ°€μ Έμ˜€κ³ 
Product product = productService.getProduct(itemCommand.productId());

// μž¬κ³ κ°μ†Œ
product.decreaseStock(itemCommand.quantity());

// OrderItem생성
OrderItem orderItem = OrderItem.create(
product.getId(),
product.getName(),
Expand All @@ -64,7 +56,6 @@ public OrderInfo createOrder(CreateOrderCommand command) {
orderItem.setOrder(order);
}

//총 κ°€κ²©κ΅¬ν•˜κ³ 
long totalAmount = order.getOrderItems().stream()
.mapToLong(OrderItem::getAmount)
.sum();
Expand All @@ -74,10 +65,39 @@ public OrderInfo createOrder(CreateOrderCommand command) {
Point point = pointService.findPointByUserId(command.userId());
point.use(totalAmount);

//μ €μž₯
Order saved = orderService.createOrder(order);
saved.updateStatus(OrderStatus.COMPLETE);

OrderPaymentCommand paymentCommand = command.payment();
String cardType = paymentCommand.cardType();
String orderReference = createOrderReference(saved.getId());
String callbackUrl = callbackBaseUrl + "/api/v1/orders/" + orderReference + "/callback";

Payment payment = Payment.pending(
saved.getId(),
command.userId(),
orderReference,
cardType,
paymentCommand.cardNo(),
saved.getTotalAmount()
);

paymentService.save(payment);
orderEventPublisher.publish(
OrderEvent.PaymentRequested.of(
saved.getId(),
command.userId(),
orderReference,
cardType,
paymentCommand.cardNo(),
saved.getTotalAmount(),
callbackUrl
)
);
Comment on lines +85 to +95
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

μ΄λ²€νŠΈμ— μΉ΄λ“œ 번호 포함 μ‹œ λ§ˆμŠ€ν‚Ήμ„ κ³ λ €ν•΄ μ£Όμ„Έμš”.

cardNoκ°€ OrderEvent.PaymentRequested μ΄λ²€νŠΈμ— ν¬ν•¨λ˜μ–΄ Kafka둜 λ°œν–‰λ©λ‹ˆλ‹€. μΉ΄λ“œ λ²ˆν˜ΈλŠ” λ―Όκ°ν•œ PII λ°μ΄ν„°λ‘œ, 이벀트 μŠ€νŠΈλ¦Όμ— ν‰λ¬ΈμœΌλ‘œ μ €μž₯되면 PCI-DSS λ“± 결제 λ³΄μ•ˆ κ·œμ • μœ„λ°˜ κ°€λŠ₯성이 μžˆμŠ΅λ‹ˆλ‹€. λ§ˆμŠ€ν‚Ή 처리(예: ****-****-****-1234)λ₯Ό ꢌμž₯ν•©λ‹ˆλ‹€.

πŸ”Ž μ œμ•ˆλœ μ ‘κ·Ό
         orderEventPublisher.publish(
                 OrderEvent.PaymentRequested.of(
                         saved.getId(),
                         command.userId(),
                         orderReference,
                         cardType,
-                        paymentCommand.cardNo(),
+                        maskCardNumber(paymentCommand.cardNo()),
                         saved.getTotalAmount(),
                         callbackUrl
                 )
         );

λ§ˆμŠ€ν‚Ή 헬퍼 λ©”μ„œλ“œ μΆ”κ°€:

private String maskCardNumber(String cardNo) {
    if (cardNo == null || cardNo.length() < 4) {
        return "****";
    }
    return "*".repeat(cardNo.length() - 4) + cardNo.substring(cardNo.length() - 4);
}

Committable suggestion skipped: line range outside the PR's diff.

πŸ€– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
around lines 85 to 95, the code publishes OrderEvent.PaymentRequested with the
raw cardNo which is sensitive; replace that with a masked version before
including it in the event. Add a private helper method (null-safe) that returns
"****" for null/too-short inputs and otherwise returns a string of asterisks for
all but the last four digits (e.g., repeat('*', length-4) + last4), then call
this helper when constructing the event so the event carries only the masked
card number.


return OrderInfo.from(saved);
}

private String createOrderReference(Long orderId) {
return "order-" + orderId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,13 @@
import com.loopers.domain.order.Order;
import com.loopers.domain.order.OrderStatus;

import java.time.LocalDateTime;
import java.util.List;

/**
* packageName : com.loopers.application.order
* fileName : OrderInfo
* author : byeonsungmun
* date : 2025. 11. 14.
* description :
* ===========================================
* DATE AUTHOR NOTE
* -------------------------------------------
* 2025. 11. 14. byeonsungmun 졜초 생성
*/
public record OrderInfo(
Long orderId,
String userId,
Long totalAmount,
OrderStatus status,
LocalDateTime createdAt,
List<OrderItemInfo> items
) {
public static OrderInfo from(Order order) {
Expand All @@ -35,7 +22,6 @@ public static OrderInfo from(Order order) {
order.getUserId(),
order.getTotalAmount(),
order.getStatus(),
order.getCreatedAt(),
itemInfos
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.loopers.application.order;

public record OrderPaymentCommand(
String cardType,
String cardNo
) {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

민감 정보 λ‘œκΉ… μ£Όμ˜κ°€ ν•„μš”ν•©λ‹ˆλ‹€.

cardNoλŠ” λ―Όκ°ν•œ 결제 μ •λ³΄μž…λ‹ˆλ‹€. Java record의 κΈ°λ³Έ toString() κ΅¬ν˜„μ€ λͺ¨λ“  ν•„λ“œλ₯Ό λ…ΈμΆœν•˜λ―€λ‘œ, λ‘œκ·Έμ— 좜λ ₯될 경우 μΉ΄λ“œλ²ˆν˜Έκ°€ ν‰λ¬ΈμœΌλ‘œ 기둝될 수 μžˆμŠ΅λ‹ˆλ‹€.

toString()을 μ˜€λ²„λΌμ΄λ“œν•˜μ—¬ λ§ˆμŠ€ν‚Ή μ²˜λ¦¬ν•˜κ±°λ‚˜, λ‘œκΉ… μ‹œ μ£Όμ˜κ°€ ν•„μš”ν•©λ‹ˆλ‹€.

πŸ”Ž λ§ˆμŠ€ν‚Ή 처리 μ˜ˆμ‹œ
 public record OrderPaymentCommand(
         String cardType,
         String cardNo
-) {}
+) {
+    @Override
+    public String toString() {
+        return "OrderPaymentCommand[cardType=" + cardType + 
+               ", cardNo=****" + (cardNo != null && cardNo.length() > 4 ? cardNo.substring(cardNo.length() - 4) : "") + "]";
+    }
+}
πŸ“ 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
public record OrderPaymentCommand(
String cardType,
String cardNo
) {}
public record OrderPaymentCommand(
String cardType,
String cardNo
) {
@Override
public String toString() {
return "OrderPaymentCommand[cardType=" + cardType +
", cardNo=****" + (cardNo != null && cardNo.length() > 4 ? cardNo.substring(cardNo.length() - 4) : "") + "]";
}
}
πŸ€– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/order/OrderPaymentCommand.java
around lines 3 to 6, the record exposes the full cardNo via the default
toString() which risks logging sensitive payment data; override the record's
toString() to either exclude cardNo or return a masked card number (e.g.,
replace all but the last 4 digits with asterisks), implement null-safety and
length checks when masking, and ensure other code paths use the masked or
excluded representation when logging (do not change equals/hashCode behavior).

Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package com.loopers.application.order;

import com.loopers.domain.order.Order;
import com.loopers.domain.order.OrderItem;
import com.loopers.domain.order.OrderService;
import com.loopers.domain.order.OrderStatus;
import com.loopers.domain.payment.Payment;
import com.loopers.domain.payment.PaymentService;
import com.loopers.domain.point.Point;
import com.loopers.domain.point.PointService;
import com.loopers.domain.product.Product;
import com.loopers.domain.product.ProductService;
import com.loopers.infrastructure.dataplatform.OrderDataPlatformClient;
import com.loopers.infrastructure.payment.PgPaymentClient;
import com.loopers.infrastructure.payment.dto.PgPaymentV1Dto;
import com.loopers.interfaces.api.ApiResponse;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
@RequiredArgsConstructor
public class OrderPaymentProcessor {

private static final Logger log = LoggerFactory.getLogger(OrderPaymentProcessor.class);

private final PaymentService paymentService;
private final OrderService orderService;
private final ProductService productService;
private final PointService pointService;
private final PgPaymentClient pgPaymentClient;
private final OrderDataPlatformClient orderDataPlatformClient;

@Transactional
public void handlePaymentCallback(String orderReference, PgPaymentV1Dto.TransactionStatus status, String transactionKey, String reason) {
Payment payment = paymentService.findByOrderReference(orderReference)
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "결제 정보λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."));

if (payment.getTransactionKey() != null && payment.getTransactionKey().equals(transactionKey)) {
return;
}

Order order = orderService.findById(payment.getOrderId());
applyPaymentResult(order, payment, status, transactionKey, reason);
}

@Transactional
public void syncPayment(Long orderId) {
Payment payment = paymentService.findByOrderId(orderId)
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "결제 정보λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."));
Order order = orderService.findById(orderId);
ApiResponse<PgPaymentV1Dto.OrderResponse> response = pgPaymentClient.getPayments(payment.getUserId(), payment.getOrderReference());
PgPaymentV1Dto.OrderResponse data = response != null ? response.data() : null;

if (data == null || data.transactions() == null || data.transactions().isEmpty()) {
log.warn("결제 내역을 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. orderId={}, orderReference={}", orderId, payment.getOrderReference());
return;
}

PgPaymentV1Dto.TransactionRecord record = data.transactions().get(data.transactions().size() - 1);
applyPaymentResult(order, payment, record.status(), record.transactionKey(), record.reason());
Comment on lines +64 to +70
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

syncPaymentμ—μ„œ 쀑볡 처리 검증 λˆ„λ½

handlePaymentCallbackμ—μ„œλŠ” transactionKey 쀑볡을 ν™•μΈν•˜μ§€λ§Œ, syncPaymentμ—μ„œλŠ” μ΅œμ‹  νŠΈλžœμž­μ…˜ λ ˆμ½”λ“œλ₯Ό 무쑰건 μ μš©ν•©λ‹ˆλ‹€. 이미 처리된 κ²°μ œκ°€ λ‹€μ‹œ 적용될 수 μžˆμŠ΅λ‹ˆλ‹€.

πŸ”Ž μ œμ•ˆν•˜λŠ” μˆ˜μ •
 PgPaymentV1Dto.TransactionRecord record = data.transactions().get(data.transactions().size() - 1);
+
+// 이미 λ™μΌν•œ transactionKey둜 처리된 경우 μŠ€ν‚΅
+if (payment.getTransactionKey() != null && payment.getTransactionKey().equals(record.transactionKey())) {
+    log.info("이미 처리된 κ²°μ œμž…λ‹ˆλ‹€. orderId={}, transactionKey={}", orderId, record.transactionKey());
+    return;
+}
+
 applyPaymentResult(order, payment, record.status(), record.transactionKey(), record.reason());
πŸ“ 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
PgPaymentV1Dto.TransactionRecord record = data.transactions().get(data.transactions().size() - 1);
applyPaymentResult(order, payment, record.status(), record.transactionKey(), record.reason());
PgPaymentV1Dto.TransactionRecord record = data.transactions().get(data.transactions().size() - 1);
// 이미 λ™μΌν•œ transactionKey둜 처리된 경우 μŠ€ν‚΅
if (payment.getTransactionKey() != null && payment.getTransactionKey().equals(record.transactionKey())) {
log.info("이미 처리된 κ²°μ œμž…λ‹ˆλ‹€. orderId={}, transactionKey={}", orderId, record.transactionKey());
return;
}
applyPaymentResult(order, payment, record.status(), record.transactionKey(), record.reason());
πŸ€– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/order/OrderPaymentProcessor.java
around lines 64-65, syncPayment currently unconditionally applies the latest
transaction record which can re-apply an already-processed payment; modify
syncPayment to first check whether the record.transactionKey() (or an equivalent
unique identifier from the record) already exists on the order or in the payment
audit/history and skip calling applyPaymentResult if it’s a duplicate, otherwise
proceed to applyPaymentResult. Ensure the duplicate check uses the same
criterion as handlePaymentCallback (transactionKey) and short-circuits before
any state changes so already-processed transactions are not applied twice.

}

@Transactional
public void handlePaymentResult(Long orderId, PgPaymentV1Dto.TransactionStatus status, String transactionKey, String reason) {
Payment payment = paymentService.findByOrderId(orderId)
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "결제 정보λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."));
Order order = orderService.findById(orderId);
applyPaymentResult(order, payment, status, transactionKey, reason);
}

private void applyPaymentResult(
Order order,
Payment payment,
PgPaymentV1Dto.TransactionStatus status,
String transactionKey,
String reason
) {
OrderStatus newStatus = OrderPaymentSupport.mapOrderStatus(status);
payment.updateStatus(OrderPaymentSupport.mapPaymentStatus(status), transactionKey, reason);
paymentService.save(payment);

if (newStatus == OrderStatus.COMPLETE) {
order.updateStatus(OrderStatus.COMPLETE);
} else if (newStatus == OrderStatus.FAIL) {
revertOrder(order, payment.getUserId());
} else {
order.updateStatus(OrderStatus.PENDING);
}

orderDataPlatformClient.send(order, payment);
}

private void revertOrder(Order order, String userId) {
for (OrderItem item : order.getOrderItems()) {
Product product = productService.getProduct(item.getProductId());
product.increaseStock(item.getQuantity());
}
Point point = pointService.findPointByUserId(userId);
if (point != null) {
point.refund(order.getTotalAmount());
}
order.updateStatus(OrderStatus.FAIL);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.loopers.application.order;

import com.loopers.domain.order.OrderStatus;
import com.loopers.domain.payment.PaymentStatus;
import com.loopers.infrastructure.payment.dto.PgPaymentV1Dto.Response;
import com.loopers.infrastructure.payment.dto.PgPaymentV1Dto.TransactionStatus;
import com.loopers.interfaces.api.ApiResponse;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;

import static com.loopers.infrastructure.payment.dto.PgPaymentV1Dto.TransactionStatus.SUCCESS;

public final class OrderPaymentSupport {

private OrderPaymentSupport() {
}

public static OrderStatus mapOrderStatus(TransactionStatus status) {
if (status == SUCCESS) {
return OrderStatus.COMPLETE;
}
if (status == TransactionStatus.FAILED) {
return OrderStatus.FAIL;
}
return OrderStatus.PENDING;
}

public static PaymentStatus mapPaymentStatus(TransactionStatus status) {
if (status == SUCCESS) {
return PaymentStatus.SUCCESS;
}
if (status == TransactionStatus.FAILED) {
return PaymentStatus.FAIL;
}
return PaymentStatus.PENDING;
}

public static Response requirePgResponse(ApiResponse<Response> response) {
Response data = response != null ? response.data() : null;
if (data == null) {
throw new CoreException(ErrorType.INTERNAL_ERROR, "PG 응닡을 확인할 수 μ—†μŠ΅λ‹ˆλ‹€.");
}
return data;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.loopers.application.user;

import com.loopers.domain.point.PointService;
import com.loopers.domain.user.User;
import com.loopers.domain.user.UserService;
import com.loopers.support.error.CoreException;
Expand All @@ -11,9 +12,11 @@
@Component
public class UserFacade {
private final UserService userService;
private final PointService pointService;

public UserInfo register(String userId, String email, String birth, String gender) {
User user = userService.register(userId, email, birth, gender);
pointService.initPoint(user.getUserId());
return UserInfo.from(user);
}
Comment on lines 19 to 23
Copy link
Copy Markdown

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:

# First, let's locate and examine the UserFacade class
find . -type f -name "UserFacade.java" | head -5

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

Length of output: 166


🏁 Script executed:

# Also find PointService and UserService to check their transaction annotations
find . -type f -name "PointService.java" -o -name "UserService.java" | head -10

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

Length of output: 239


🏁 Script executed:

# Search for @Transactional annotations in the user package
rg -n "@Transactional" --type java -g "*/user/*" | head -20

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

Length of output: 236


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java

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

Length of output: 1310


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java

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

Length of output: 2463


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java

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

Length of output: 1219


🏁 Script executed:

# Search for @Transactional across the entire codebase
rg -n "@Transactional" --type java | head -30

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

Length of output: 3592


UserFacade.register() λ©”μ„œλ“œμ— @Transactional μΆ”κ°€ ν•„μš”

ν˜„μž¬ UserFacade.register()λŠ” νŠΈλžœμž­μ…˜ 경계가 μ—†μ–΄ 각 μ„œλΉ„μŠ€ λ©”μ„œλ“œκ°€ 독립적인 νŠΈλžœμž­μ…˜μ„ μƒμ„±ν•©λ‹ˆλ‹€. userService.register()κ°€ μ»€λ°‹λœ ν›„ pointService.initPoint()κ°€ μ‹€νŒ¨ν•˜λ©΄ μ‚¬μš©μžλŠ” μƒμ„±λ˜μ§€λ§Œ 포인트 μ΄ˆκΈ°ν™”λŠ” μ‹€νŒ¨ν•˜μ—¬ 데이터 λΆˆμΌμΉ˜κ°€ λ°œμƒν•©λ‹ˆλ‹€. λ©”μ„œλ“œμ— @Transactional을 μ μš©ν•˜μ—¬ 두 μž‘μ—…μ„ ν•˜λ‚˜μ˜ νŠΈλžœμž­μ…˜μœΌλ‘œ λ¬ΆμœΌμ„Έμš”.

πŸ€– Prompt for AI Agents
In apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java
around lines 17 to 21, the register method lacks a transaction boundary so user
creation and point initialization run in separate transactions; annotate the
method with @Transactional (importing
org.springframework.transaction.annotation.Transactional) so both
userService.register(...) and pointService.initPoint(...) execute within the
same transaction, ensuring a rollback if point initialization fails (ensure the
class is a Spring component/@Service or otherwise proxy-able and that runtime
exceptions will trigger rollback).


Expand Down
Loading