Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a90b419
test: 회원가입 테스트 작성
incheol789 Feb 2, 2026
01f9a27
feat: 회원가입 기능 구현
incheol789 Feb 2, 2026
e3501eb
test: 내 정보 조회 테스트 작성
incheol789 Feb 2, 2026
d582233
feat: 내 정보 조회 기능 구현
incheol789 Feb 2, 2026
f2b25a7
test: 비밀번호 수정 테스트 작성
incheol789 Feb 2, 2026
264c468
feat: 비밀번호 수정 기능 구현
incheol789 Feb 2, 2026
ccbd991
fix: 누락된 요청 헤더 예외 처리 추가
incheol789 Feb 2, 2026
f64462d
docs: 회원 API http 파일 작성
incheol789 Feb 2, 2026
c55702a
Merge branch 'Loopers-dev-lab:main' into main
incheol789 Feb 5, 2026
29f2362
Merge branch 'Loopers-dev-lab:main' into round-3
incheol789 Feb 15, 2026
c4ee07d
chore: Example 도메인 코드 및 테스트 제거
Feb 26, 2026
5443b24
feat: Money 값 객체 및 단위 테스트 구현
Feb 26, 2026
5fdbf76
feat: 브랜드 도메인, Repository, 단위 테스트 구현
Feb 26, 2026
2111134
feat: 상품 도메인, Repository, 단위 테스트 구현
Feb 26, 2026
3045da9
feat: 좋아요 도메인, Repository, 단위 테스트 구현
Feb 26, 2026
52e1b44
feat: 포인트 도메인, Repository, 단위 테스트 구현
Feb 26, 2026
c15f5e1
feat: 주문 도메인, Repository, 단위 테스트 구현
Feb 26, 2026
51e2529
feat: 회원 포인트 충전 및 조회 기능 구현
Feb 26, 2026
b6437c8
feat: 브랜드 Facade 및 API 레이어 구현
Feb 26, 2026
4fef080
feat: 상품 Facade 및 API 레이어 구현
Feb 26, 2026
08cea89
feat: 좋아요 Facade 및 API 레이어 구현
Feb 26, 2026
4694c57
feat: 주문 Facade 및 API 레이어 구현
Feb 26, 2026
327c9f3
fix: ApiControllerAdvice 불필요한 코드 제거
Feb 26, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.loopers.application.brand;

import com.loopers.domain.brand.BrandModel;
import com.loopers.domain.brand.BrandService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
@RequiredArgsConstructor
public class BrandFacade {

private final BrandService brandService;

@Transactional
public BrandInfo register(String name, String description, String imageUrl) {
BrandModel brand = brandService.register(name, description, imageUrl);
return BrandInfo.from(brand);
}

@Transactional(readOnly = true)
public BrandInfo getById(Long id) {
BrandModel brand = brandService.getById(id);
return BrandInfo.from(brand);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.loopers.application.brand;

import com.loopers.domain.brand.BrandModel;

public record BrandInfo(
Long id,
String name,
String description,
String imageUrl
) {
public static BrandInfo from(BrandModel model) {
return new BrandInfo(
model.getId(),
model.getName(),
model.getDescription(),
model.getImageUrl()
);
}
}

This file was deleted.

This file was deleted.

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

import com.loopers.domain.like.ProductLikeService;
import com.loopers.domain.member.MemberModel;
import com.loopers.domain.member.MemberService;
import com.loopers.domain.product.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Component
public class LikeFacade {

private final ProductLikeService productLikeService;
private final MemberService memberService;
private final ProductService productService;

@Transactional
public void addLike(String loginId, String password, Long productId) {
MemberModel member = memberService.getMyInfo(loginId, password);
productService.getById(productId);
productLikeService.addLike(member.getId(), productId);
}

@Transactional
public void removeLike(String loginId, String password, Long productId) {
MemberModel member = memberService.getMyInfo(loginId, password);
productService.getById(productId);
productLikeService.removeLike(member.getId(), productId);
}

@Transactional(readOnly = true)
public long countByProductId(Long productId) {
return productLikeService.countByProductId(productId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.loopers.application.member;

import com.loopers.domain.member.MemberModel;
import com.loopers.domain.member.MemberService;
import com.loopers.domain.point.PointModel;
import com.loopers.domain.point.PointService;
import com.loopers.domain.vo.Money;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;

@RequiredArgsConstructor
@Component
public class MemberFacade {

private final MemberService memberService;
private final PointService pointService;

@Transactional
public void register(String loginId, String password, String name, LocalDate birthDate, String email) {
MemberModel member = memberService.register(loginId, password, name, birthDate, email);
pointService.createPoint(member.getId());
}

@Transactional(readOnly = true)
public MemberInfo getMyInfo(String loginId, String password) {
MemberModel member = memberService.getMyInfo(loginId, password);
PointModel point = pointService.getByMemberId(member.getId());
return MemberInfo.of(member, point.getBalanceMoney());
}

@Transactional
public void chargePoint(String loginId, String password, long amount) {
MemberModel member = memberService.getMyInfo(loginId, password);
pointService.charge(member.getId(), Money.of(amount));
}

@Transactional
public void changePassword(String loginId, String currentPassword, String newPassword) {
memberService.changePassword(loginId, currentPassword, newPassword);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.loopers.application.member;

import com.loopers.domain.member.MemberModel;
import com.loopers.domain.vo.Money;

import java.time.LocalDate;

public record MemberInfo(
String loginId,
String maskedName,
LocalDate birthDate,
String email,
Money point
) {
public static MemberInfo of(MemberModel model, Money point) {
return new MemberInfo(
model.getLoginId(),
model.getMaskedName(),
model.getBirthDate(),
model.getEmail(),
point
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.loopers.application.order;

import com.loopers.domain.brand.BrandService;
import com.loopers.domain.member.MemberModel;
import com.loopers.domain.member.MemberService;
import com.loopers.domain.order.OrderItemModel;
import com.loopers.domain.order.OrderModel;
import com.loopers.domain.order.OrderService;
import com.loopers.domain.point.PointService;
import com.loopers.domain.product.ProductModel;
import com.loopers.domain.product.ProductService;
import com.loopers.domain.vo.Money;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

@RequiredArgsConstructor
@Component
public class OrderFacade {

private final OrderService orderService;
private final MemberService memberService;
private final ProductService productService;
private final BrandService brandService;
private final PointService pointService;

@Transactional
public OrderInfo createOrder(String loginId, String password, List<OrderItemRequest> itemRequests) {
// 1. 회원 인증
MemberModel member = memberService.getMyInfo(loginId, password);

// 2. 주문 항목 생성 (상품 스냅샷 + 재고 차감)
List<OrderItemModel> orderItems = new ArrayList<>();
for (OrderItemRequest req : itemRequests) {
ProductModel product = productService.getById(req.productId());
productService.decreaseStock(product.getId(), req.quantity());
Comment on lines +31 to +39
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

주문 항목 입력 검증 부재로 500 응답이 발생할 수 있다.

운영 환경에서 itemRequestsnull/empty이거나 항목의 productIdnull, quantity <= 0이면 현재는 NPE 또는 하위 예외로 흘러 일관된 오류 포맷이 깨질 수 있다. 메서드 시작 시 CoreException(BAD_REQUEST)로 명시 검증을 추가하기 바란다.
추가 테스트로 null, 빈 리스트, null productId, 0/음수 quantity 입력에 대한 createOrder 예외 케이스를 단위 테스트로 보강하기 바란다.

🔧 제안 수정안
 `@Transactional`
 public OrderInfo createOrder(String loginId, String password, List<OrderItemRequest> itemRequests) {
+    if (itemRequests == null || itemRequests.isEmpty()) {
+        throw new CoreException(ErrorType.BAD_REQUEST, "주문 항목은 1개 이상이어야 한다.");
+    }
+    for (OrderItemRequest req : itemRequests) {
+        if (req == null || req.productId() == null || req.quantity() <= 0) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "주문 항목 정보가 올바르지 않다.");
+        }
+    }
     // 1. 회원 인증
     MemberModel member = memberService.getMyInfo(loginId, password);

Based on learnings: In the loop-pack-be-l2-vol3-java project, enforce unified error handling by routing errors through CoreException to ApiControllerAdvice to ensure a consistent response format.

📝 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 OrderInfo createOrder(String loginId, String password, List<OrderItemRequest> itemRequests) {
// 1. 회원 인증
MemberModel member = memberService.getMyInfo(loginId, password);
// 2. 주문 항목 생성 (상품 스냅샷 + 재고 차감)
List<OrderItemModel> orderItems = new ArrayList<>();
for (OrderItemRequest req : itemRequests) {
ProductModel product = productService.getById(req.productId());
productService.decreaseStock(product.getId(), req.quantity());
public OrderInfo createOrder(String loginId, String password, List<OrderItemRequest> itemRequests) {
if (itemRequests == null || itemRequests.isEmpty()) {
throw new CoreException(ErrorType.BAD_REQUEST, "주문 항목은 1개 이상이어야 한다.");
}
for (OrderItemRequest req : itemRequests) {
if (req == null || req.productId() == null || req.quantity() <= 0) {
throw new CoreException(ErrorType.BAD_REQUEST, "주문 항목 정보가 올바르지 않다.");
}
}
// 1. 회원 인증
MemberModel member = memberService.getMyInfo(loginId, password);
// 2. 주문 항목 생성 (상품 스냅샷 + 재고 차감)
List<OrderItemModel> orderItems = new ArrayList<>();
for (OrderItemRequest req : itemRequests) {
ProductModel product = productService.getById(req.productId());
productService.decreaseStock(product.getId(), req.quantity());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java`
around lines 31 - 39, Add explicit input validation at the start of createOrder:
if itemRequests is null or empty, or any OrderItemRequest has a null productId
or quantity <= 0, throw a CoreException with BAD_REQUEST; keep the rest of the
flow (memberService.getMyInfo, productService.getById,
productService.decreaseStock, building orderItems) unchanged and ensure all
error paths use CoreException so ApiControllerAdvice produces a consistent
response. Update unit tests for createOrder to cover null itemRequests, empty
list, an item with null productId, and items with quantity 0 or negative to
assert CoreException(BAD_REQUEST) is thrown.


OrderItemModel item = new OrderItemModel(
product.getId(),
product.getName(),
brandService.getById(product.getBrandId()).getName(),
product.getPrice(),
req.quantity()
);
orderItems.add(item);
}

// 3. 주문 생성
OrderModel order = orderService.createOrder(member.getId(), orderItems);

// 4. 포인트 차감
pointService.use(member.getId(), Money.of(order.getTotalAmount()));

List<OrderItemModel> savedItems = orderService.getOrderItems(order.getId());
return OrderInfo.from(order, savedItems);
}

@Transactional(readOnly = true)
public OrderInfo getById(Long id) {
OrderModel order = orderService.getById(id);
List<OrderItemModel> items = orderService.getOrderItems(order.getId());
return OrderInfo.from(order, items);
Comment on lines +61 to +65
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

주문 단건 조회에 소유권 검증이 없어 타회원 주문이 노출될 수 있다.

현재는 주문 ID만 알면 누구의 주문이든 조회 가능한 구조라 개인정보/주문정보 유출 위험이 있다. 조회 시 요청자 식별 정보를 받아 order.memberId == requesterId를 검증하고, 불일치 시 권한 오류를 반환하도록 수정하기 바란다.
추가 테스트로 회원 A의 주문을 회원 B가 조회할 때 권한 오류가 발생하는 케이스를 API/파사드 테스트에 포함하기 바란다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java`
around lines 61 - 65, Modify OrderFacade.getById to enforce ownership: accept
the requester identity (e.g., add a Long requesterId parameter or obtain it from
the security context inside getById), call orderService.getById(id) to load the
OrderModel, then compare order.getMemberId() with requesterId and throw an
authorization exception (e.g., AccessDeniedException or your app's equivalent)
if they differ before calling orderService.getOrderItems(...) and
OrderInfo.from(...); also add an API/facade test that attempts to read Member
A's order as Member B and asserts the authorization error is returned.

}

@Transactional(readOnly = true)
public List<OrderInfo> getByMember(String loginId, String password) {
MemberModel member = memberService.getMyInfo(loginId, password);
List<OrderModel> orders = orderService.getByMemberId(member.getId());

return orders.stream()
.map(order -> {
List<OrderItemModel> items = orderService.getOrderItems(order.getId());
return OrderInfo.from(order, items);
})
.toList();
}

public record OrderItemRequest(
Long productId,
int quantity
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.loopers.application.order;

import com.loopers.domain.order.OrderItemModel;
import com.loopers.domain.order.OrderModel;

import java.util.List;

public record OrderInfo(
Long id,
Long memberId,
int totalAmount,
List<OrderItemInfo> items
) {
public record OrderItemInfo(
Long productId,
String productName,
String brandName,
int productPrice,
int quantity,
int totalAmount
) {
public static OrderItemInfo from(OrderItemModel model) {
return new OrderItemInfo(
model.getProductId(),
model.getProductName(),
model.getBrandName(),
model.getProductPrice(),
model.getQuantity(),
model.getTotalAmount()
);
}
}

public static OrderInfo from(OrderModel order, List<OrderItemModel> items) {
return new OrderInfo(
order.getId(),
order.getMemberId(),
order.getTotalAmount(),
items.stream().map(OrderItemInfo::from).toList()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.loopers.application.product;

import com.loopers.domain.brand.BrandModel;
import com.loopers.domain.product.ProductModel;

public record ProductDetailInfo(
Long id,
String name,
String description,
int price,
int stockQuantity,
String imageUrl,
String brandName,
long likeCount
) {
public static ProductDetailInfo of(ProductModel product, BrandModel brand, long likeCount) {
return new ProductDetailInfo(
product.getId(),
product.getName(),
product.getDescription(),
product.getPrice(),
product.getStockQuantity(),
product.getImageUrl(),
brand.getName(),
likeCount
);
}
}
Loading