Skip to content

Conversation

@leeminkyu-kr96
Copy link
Collaborator

@leeminkyu-kr96 leeminkyu-kr96 commented Nov 13, 2025

📌 Summary

상품(Product), 브랜드(Brand), 좋아요(Like), 주문(Order) 도메인 기능 구현 및 테스트 코드 작성

💬 리뷰 포인트

  • 시간이 부족하여 도메인 레이어 이후에 많은 부분을 ai로 만들어서 제출했습니다. 이러한 부분이 너무 아쉽고 현타가 오지만 다음 과제 떄는 좀 더 노력해보겠습니다.
  • 위와 같은 이유로, 가급적 도메인 레이어만 중점적으로 봐주시면 정말 감사하겠습니다.

✅ Checklist

🏷 Product / Brand 도메인
[x] 상품 정보 객체는 브랜드 정보, 좋아요 수를 포함한다.
[x] 상품의 정렬 조건(latest, price_asc, likes_desc) 을 고려한 조회 기능을 설계했다
[x] 상품은 재고를 가지고 있고, 주문 시 차감할 수 있어야 한다
[x] 재고는 감소만 가능하며 음수 방지는 도메인 레벨에서 처리된다

👍 Like 도메인
[x] 좋아요는 유저와 상품 간의 관계로 별도 도메인으로 분리했다
[x] 중복 좋아요 방지를 위한 멱등성 처리가 구현되었다
[x] 상품의 좋아요 수는 상품 상세/목록 조회에서 함께 제공된다
[x] 단위 테스트에서 좋아요 등록/취소/중복 방지 흐름을 검증했다

🛒 Order 도메인
[x] 주문은 여러 상품을 포함할 수 있으며, 각 상품의 수량을 명시한다
[x] 주문 시 상품의 재고 차감, 유저 포인트 차감 등을 수행한다
[x] 재고 부족, 포인트 부족 등 예외 흐름을 고려해 설계되었다
[x] 단위 테스트에서 정상 주문 / 예외 주문 흐름을 모두 검증했다

🧩 도메인 서비스
[x] 도메인 간 협력 로직은 Domain Service에 위치시켰다
[x] 상품 상세 조회 시 Product + Brand 정보 조합은 도메인 서비스에서 처리했다
[x] 복합 유스케이스는 Application Layer에 존재하고, 도메인 로직은 위임되었다
[x] 도메인 서비스는 상태 없이, 도메인 객체의 협력 중심으로 설계되었다

🧱 소프트웨어 아키텍처 & 설계
[ ] 전체 프로젝트의 구성은 아래 아키텍처를 기반으로 구성되었다 (Application → Domain ← Infrastructure)
[ ] Application Layer는 도메인 객체를 조합해 흐름을 orchestration 했다
[ ] 핵심 비즈니스 로직은 Entity, VO, Domain Service 에 위치한다
[ ] Repository Interface는 Domain Layer 에 정의되고, 구현체는 Infra에 위치한다
[ ] 패키지는 계층 + 도메인 기준으로 구성되었다 (/domain/order, /application/like 등)
[ ] 테스트는 외부 의존성을 분리하고, Fake/Stub 등을 사용해 단위 테스트가 가능하게 구성되었다

@coderabbitai
Copy link

coderabbitai bot commented Nov 13, 2025

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

풀 리퀘스트 분석

Walkthrough

전자상거래 플랫폼을 위한 완전한 도메인 구조 구현. 사용자, 상품, 브랜드, 좋아요, 주문, 포인트에 관한 도메인 모델, 서비스, 레포지토리 계층 추가. 애플리케이션 파사드, REST API 컨트롤러, DTO 계층 신규 구성. 설계 문서 및 E2E 테스트 포함.

Changes

응집 / 파일(들) 변경 요약
설계 문서
.docs/design/01-requirements.md, .docs/design/02-sequence-diagrams.md, .docs/design/03-class-diagram.md, .docs/design/04-erd.md
제품 조회, 상세 정보 검색, 브랜드 필터링, 좋아요 기능, 주문 생성에 대한 요구사항 정의. 시퀀스 다이어그램, 클래스 다이어그램, ER 다이어그램 추가
공통 도메인 값 객체
apps/commerce-api/src/main/java/com/loopers/domain/common/Money.java, ...Quantity.java
음수 검증이 있는 불변 값 객체. JPA 임베더블로 설정
사용자 도메인
apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java, ...Email.java, ...Gender.java, ...BirthDate.java, ...UserModel.java, ...UserRepository.java, ...UserService.java
사용자 엔티티, 임베더블 값 객체(UserId, Email 등), 레포지토리 인터페이스, 회원가입 및 조회 로직이 있는 서비스
상품 도메인
apps/commerce-api/src/main/java/com/loopers/domain/product/Brand.java, ...ProductModel.java, ...ProductRepository.java, ...ProductService.java
상품 엔티티, 브랜드 값 객체, 재고 감소 및 좋아요 수 관리, 페이징/필터링 조회
좋아요 도메인
apps/commerce-api/src/main/java/com/loopers/domain/like/LikeModel.java, ...LikeRepository.java, ...LikeService.java
사용자-상품 좋아요 관계 JPA 엔티티, 복합 unique 제약, 토글/추가/제거/개수 조회
주문 도메인
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderModel.java, ...OrderItemModel.java, ...OrderRepository.java, ...OrderService.java
주문 및 주문 항목 엔티티, 재고 차감/포인트 차감 및 주문 생성 조율 로직 포함
포인트 도메인
apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java, ...PointRepository.java, ...PointService.java
포인트 엔티티, 충전 및 사용 로직, 잔액 검증
애플리케이션 파사드
apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.java, ...LikeFacade.java, ...OrderFacade.java, ...PointFacade.java, ...ProductFacade.java, ...UserFacade.java
각 도메인에 대한 트랜잭션 파사드, 입력 검증, 도메인 모델 접근 제어
애플리케이션 DTO
apps/commerce-api/src/main/java/com/loopers/application/*/[Brand|Like|Order|Point|Product|User]Info.java
도메인 모델을 응용 계층 응답 타입으로 매핑하는 레코드 및 팩토리 메서드
JPA 레포지토리 구현
apps/commerce-api/src/main/java/com/loopers/infrastructure/[brand|like|order|point|product|user]/\*JpaRepository.java, ...\*RepositoryImpl.java
스프링 데이터 JPA 레포지토리 인터페이스 및 도메인 레포지토리 인터페이스 구현
API 명세 및 컨트롤러
apps/commerce-api/src/main/java/com/loopers/interfaces/api/[brand|like|order|point|product|user]/\*V1ApiSpec.java, ...\*V1Controller.java, ...\*V1Dto.java
RESTful 엔드포인트, Swagger 주석, 요청/응답 DTO, 헤더 및 본문 검증
API 에러 핸들링
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java
MissingRequestHeaderException, MethodArgumentNotValidException 처리 추가
도메인 및 통합 테스트
apps/commerce-api/src/test/java/com/loopers/domain/[like|order|point|product|user]/\*Test.java, ...IntegrationTest.java
단위 테스트, 도메인 로직 검증, 재고/포인트 차감 시나리오
E2E API 테스트
apps/commerce-api/src/test/java/com/loopers/interfaces/api/\*V1ApiE2ETest.java
REST 엔드포인트에 대한 전체 흐름 테스트, 헤더 검증, 에러 케이스

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant OrderV1Controller
    participant OrderFacade
    participant OrderService
    participant ProductService
    participant PointService
    participant Repository

    Client->>OrderV1Controller: POST /api/v1/orders (X-USER-ID, CreateOrderRequest)
    OrderV1Controller->>OrderFacade: createOrder(userId, items)
    OrderFacade->>OrderService: createOrder(user, itemRequests)
    
    rect rgb(200, 220, 255)
        Note over OrderService: 트랜잭션 블록
        loop 각 주문 항목
            OrderService->>ProductService: getProduct(productId)
            ProductService-->>OrderService: ProductModel
            OrderService->>ProductService: updateQuantity(productId, qty)
        end
        OrderService->>OrderService: 총 가격 계산
        OrderService->>PointService: use(user, totalPrice)
        OrderService->>Repository: save(orderModel)
    end
    
    Repository-->>OrderService: savedOrder
    OrderService-->>OrderFacade: OrderModel
    OrderFacade-->>OrderV1Controller: OrderInfo
    OrderV1Controller-->>Client: ApiResponse(OrderResponse)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

세부 검토 대상:

  • OrderService.createOrder(): 주문 생성 시 재고 감소, 포인트 차감, 주문 저장이 단일 트랜잭션 내에서 발생. 트랜잭션 경계와 롤백 시나리오 검토 필요
  • LikeService 토글 로직: addLike/removeLike/toggleLike 구현이 서로 다른 의도를 반영하는지 확인 필요 (addLike는 기존 좋아요 시 제거, removeLike는 기존 좋아요 없으면 생성)
  • ProductModel.decreaseQuantity(): 재고 부족 시 예외 처리 및 검증 로직
  • 값 객체 검증: Money, Quantity, UserId, Email, BirthDate의 생성자 검증 규칙이 일관되는지 확인
  • 파사드 입력 검증: BrandFacade.getBrand()에서 null/blank 체크; 모든 파사드에서 NOT_FOUND 예외 처리 일관성 검토
  • JPA 쿼리: countByProductIdsLiked의 @Query 구현이 정확한지 검증
  • E2E 테스트: 전체 주문 흐름, 포인트 차감, 재고 감소 통합 테스트 커버리지 검토

Possibly related PRs

  • #38: 포인트 및 사용자 관련 애플리케이션 코드(PointFacade/PointInfo, PointService/PointRepository, 컨트롤러/DTO 및 테스트) 추가
  • #16: 사용자 도메인 및 사용자 관리 API 구현(UserService, UserRepository, 파사드/컨트롤러/DTO 및 관련 테스트)
  • #51: 동일한 설계 문서 파일(.docs/design/01-04) 및 시퀀스/클래스/ERD 문서 수정

Poem

🐰 제품과 주문이 춤을 추고,
포인트가 반짝이며 좋아요가 모여드네,
사용자의 이야기가 데이터베이스에 새겨지고,
아름다운 구조로 e-커머스가 피어난다네! 🛍️✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.06% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed PR 설명은 필수 섹션(Summary, Review Points, Checklist)을 모두 포함하고 있으며, 도메인 레이어 구현 내용과 테스트 검증 항목이 체계적으로 작성되어 있습니다.
Title check ✅ Passed PR 제목은 '상품, 브랜드, 좋아요, 주문 도메인 구현'으로 변경 사항의 주요 내용을 정확하게 요약하고 있습니다.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 27

♻️ Duplicate comments (1)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java (1)

20-28: @notblank 검증이 UserId 타입에서 작동하지 않을 수 있습니다.

@NotBlankCharSequence 타입에만 적용됩니다. UserId가 커스텀 타입인 경우 @NotNull을 사용하거나 UserId 내부에서 검증을 수행해야 합니다. (LikeV1Controller와 동일한 문제)

🧹 Nitpick comments (31)
.docs/design/03-class-diagram.md (1)

5-9: 포인트(point) 필드 타입 검토

User 클래스의 point 필드가 int 타입으로 정의되어 있습니다. 포인트 적립이 누적되는 비즈니스 로직이라면 int 범위 초과로 인한 오버플로우 위험이 있습니다.

  • 예상 최대 포인트 규모를 고려하여 long 타입 사용을 검토하세요.
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java (1)

7-13: 메서드 이름과 파라미터 타입을 일치시켜 주세요.
현재 findByUserIdUserModel을 파라미터로 받아 이름과 구현 의도가 어긋납니다. 계약을 읽는 다른 개발자가 혼란을 겪기 쉬우니, 메서드명을 findByUser 등으로 바꾸거나 UserId를 받도록 조정하는 편이 좋겠습니다.

-    List<OrderModel> findByUserId(UserModel user);
+    List<OrderModel> findByUser(UserModel user);
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java (1)

60-64: 필드명을 포함해 검증 오류 메시지를 전달하면 이해가 쉬워집니다.
기본 메시지만 이어붙이면 어떤 필드에서 오류가 발생했는지 파악하기 어렵습니다. 필드명을 함께 노출하고 메시지가 비어 있을 때는 코드 등으로 대체하면 클라이언트와 로그 양쪽 모두에서 진단이 빨라집니다.

-        String errorMessage = e.getBindingResult().getFieldErrors().stream()
-            .map(FieldError::getDefaultMessage)
+        String errorMessage = e.getBindingResult().getFieldErrors().stream()
+            .map(fieldError -> {
+                String message = fieldError.getDefaultMessage();
+                return "%s: %s".formatted(fieldError.getField(), message != null ? message : fieldError.getCode());
+            })
             .collect(Collectors.joining(", "));
apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java (2)

18-18: 사용하지 않는 import를 제거하세요.

Line 18의 java.time.LocalDate import는 코드에서 사용되지 않습니다.

-import java.time.LocalDate;

39-39: 포인트 사용 테스트가 누락되었습니다.

TODO 주석이 포인트 사용(차감) 기능에 대한 테스트가 필요함을 나타냅니다. PointModel이 포인트 사용 메서드를 제공한다면, 해당 기능에 대한 테스트를 추가해야 합니다.

포인트 사용 테스트 코드를 생성하거나 이를 추적할 이슈를 생성하길 원하시나요?

apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java (1)

10-10: 값 객체 타입을 일관되게 사용하세요.

findByUserId(Long id)의 파라미터 타입이 Long이지만, UserJpaRepository.findByUserId(UserId userId)UserId 값 객체를 사용합니다. 도메인 일관성과 타입 안정성을 위해 UserId 값 객체를 사용하세요.

다음과 같이 수정하세요:

-List<OrderModel> findByUserId(Long id);
+List<OrderModel> findByUser_UserId(UserId userId);

참고: Spring Data JPA는 중첩 속성 쿼리를 위해 언더스코어(_) 표기법을 지원합니다. OrderModeluser.userId 경로를 쿼리하려면 findByUser_UserId를 사용하세요.

apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.java (1)

14-16: 중복 검증 로직을 제거하세요.

BrandFacade에서 brandName의 null/blank 검증을 수행하지만, Brand 생성자(Line 18)에서도 동일한 검증을 수행할 가능성이 높습니다. 파사드 계층에서의 중복 검증은 불필요합니다.

도메인 객체(Brand)의 생성자에서 검증을 담당하도록 하고, 파사드에서는 도메인 로직을 호출하기만 하세요.

 @Transactional(readOnly = true)
 public BrandInfo getBrand(String brandName) {
-    if (brandName == null || brandName.isBlank()) {
-        throw new CoreException(ErrorType.BAD_REQUEST, "브랜드 이름은 필수입니다.");
-    }
-    
     Brand brand = new Brand(brandName);
     return BrandInfo.from(brand);
 }
apps/commerce-api/src/main/java/com/loopers/domain/user/Gender.java (1)

14-20: 성별 값에 대한 검증 로직 추가를 권장합니다.

현재 null/blank 체크만 수행하고 있지만, EmailBirthDate처럼 허용 가능한 성별 값("male", "female" 등)에 대한 검증이 필요합니다. 현재는 임의의 문자열(예: "xyz", "123")도 허용됩니다.

다음과 같이 검증 로직 추가를 고려하세요:

 public Gender(String gender) {
     //성별 체크
     if (gender == null || gender.isBlank()) {
         throw new CoreException(ErrorType.BAD_REQUEST, "성별은 비어있을 수 없습니다.");
     }
+    if (!gender.matches("^(male|female|other)$")) {
+        throw new CoreException(ErrorType.BAD_REQUEST, "성별은 'male', 'female', 'other' 중 하나여야 합니다.");
+    }
     this.gender = gender;
 }
apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java (1)

7-16: 도메인 Value Object가 애플리케이션 레이어에 노출되고 있습니다.

BrandMoney가 도메인 Value Object인데 ProductInfo에 직접 노출되고 있습니다. 애플리케이션 레이어 DTO는 일반적으로 원시 타입이나 단순 타입을 사용하여 도메인 의존성을 최소화하는 것이 좋습니다.

다음과 같이 단순 타입으로 변환하는 것을 고려하세요:

-public record ProductInfo(Long id, String name, Brand brand, Money price, Long likeCount) {
+public record ProductInfo(Long id, String name, String brand, Integer price, Long likeCount) {
     public static ProductInfo from(ProductModel model) {
         return new ProductInfo(
             model.getId(),
             model.getName(),
-            model.getBrand(),
-            model.getPrice(),
+            model.getBrand().name(),
+            model.getPrice().value(),
             model.getLikeCount()
         );
     }
 }
apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java (1)

51-53: 불필요한 중복 검증을 제거하세요.

Line 53의 findById().orElseThrow()는 실제 테스트 대상인 Line 57 productService.getProduct(id)와 동일한 로직을 중복 검증합니다. arrange 단계에서는 데이터 준비만 하고, 실제 검증은 act/assert 단계에서 수행해야 합니다.

         void productService_whenGetProductIsNotFound() {
             // arrange
             Long id = 1L;
-            productJpaRepository.findById(id).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품이 존재하지 않습니다."));

             // act
             CoreException result = assertThrows(CoreException.class, () -> {
apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java (1)

11-13: 중복된 접근자 메서드입니다.

레코드는 이미 point() 접근자를 자동으로 생성하므로, getPoint() 메서드는 중복입니다. JavaBeans 스타일 getter가 특정 프레임워크에서 필요한 경우가 아니라면 제거하는 것이 좋습니다.

중복 제거를 원하신다면 다음 diff를 적용하세요:

-    public Money getPoint() {
-        return point;
-    }
apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java (1)

25-31: 에러 메시지를 더 구체적으로 개선할 수 있습니다.

"존재하지 않는 요청입니다."는 너무 일반적인 메시지입니다. 사용자를 찾을 수 없다는 것을 명확히 표현하면 디버깅과 사용자 경험이 개선됩니다.

더 구체적인 메시지로 변경:

         if (user == null) {
-            throw new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 요청입니다.");
+            throw new CoreException(ErrorType.NOT_FOUND, "사용자를 찾을 수 없습니다.");
         }
apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java (1)

28-34: equals 구현을 단순화할 수 있습니다.

현재 null 체크에 삼항 연산자를 사용하고 있지만, Objects.equals()를 사용하면 더 간결합니다.

다음 diff를 적용하여 equals를 개선하세요:

+import java.util.Objects;
+
 @Override
 public boolean equals(Object o) {
     if (this == o) return true;
     if (o == null || getClass() != o.getClass()) return false;
     UserId userId1 = (UserId) o;
-    return userId != null ? userId.equals(userId1.userId) : userId1.userId == null;
+    return Objects.equals(userId, userId1.userId);
 }
apps/commerce-api/src/main/java/com/loopers/domain/user/BirthDate.java (2)

14-23: 형식 검증은 적절하나 날짜 유효성은 검증하지 않습니다.

현재 정규식은 yyyy-MM-dd 형식만 검증하지만, 실제 날짜 유효성(예: 2025-13-99)은 검증하지 않습니다. 현재 구현도 동작하지만, 더 엄격한 검증이 필요하다면 LocalDate.parse()를 사용할 수 있습니다.

날짜 유효성까지 검증하려면 다음 diff를 적용하세요:

+import java.time.LocalDate;
+import java.time.format.DateTimeParseException;
+
 public BirthDate(String birthDate) {
     //생년월일 validation check
     if (birthDate == null || birthDate.isBlank()) {
         throw new CoreException(ErrorType.BAD_REQUEST, "생년월일은 비어있을 수 없습니다.");
     }
-    if (!birthDate.matches("^\\d{4}-\\d{2}-\\d{2}$")) {
+    try {
+        LocalDate.parse(birthDate);
+    } catch (DateTimeParseException e) {
         throw new CoreException(ErrorType.BAD_REQUEST, "생년월일이 `yyyy-MM-dd` 형식에 맞아야 합니다.");
     }
     this.birthDate = birthDate;
 }

29-35: equals 구현을 단순화할 수 있습니다.

Objects.equals()를 사용하면 더 간결하고 읽기 쉬운 코드가 됩니다.

다음 diff를 적용하세요:

+import java.util.Objects;
+
 @Override
 public boolean equals(Object o) {
     if (this == o) return true;
     if (o == null || getClass() != o.getClass()) return false;
     BirthDate birthDate1 = (BirthDate) o;
-    return birthDate != null ? birthDate.equals(birthDate1.birthDate) : birthDate1.birthDate == null;
+    return Objects.equals(birthDate, birthDate1.birthDate);
 }
apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (2)

20-33: 포인트 조회 로직을 개선하세요.

getPoint 메서드에서 new PointModel(user, new Money(0))를 생성하는 것은 불필요하고 혼란스럽습니다. PointService.findPointPointModel을 인자로 받지만 실제로는 user 필드만 사용하므로, 서비스 메서드 시그니처를 findPoint(UserModel user) 또는 findByUser(UserModel user)로 변경하는 것을 권장합니다.

다음과 같이 개선할 수 있습니다:

PointService.java 수정:

-public PointModel findPoint(PointModel point) {
-    UserModel requestUser = point.getUser();
+public PointModel findByUser(UserModel user) {
     var foundUser = userRepository.findById(requestUser.getId());
     if (foundUser == null) {
         return null;
     }
     return pointRepository.findPoint(foundUser.get()).orElse(null);
 }

PointFacade.java 수정:

 public PointInfo getPoint(UserId userId) {
     UserModel user = userService.getUser(userId);
     if (user == null) {
         throw new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 요청입니다.");
     }
-    PointModel pointModel = new PointModel(user, new Money(0));
-    PointModel point = pointService.findPoint(pointModel);
+    PointModel point = pointService.findByUser(user);
     
     if (point == null) {
         throw new CoreException(ErrorType.NOT_FOUND, "포인트 정보가 없습니다.");
     }
     
     return PointInfo.from(point);
 }

35-45: 포인트 충전 후 불필요한 조회를 제거하세요.

충전 후 다시 포인트를 조회하는 로직(lines 43-44)이 비효율적입니다. PointService.charge 메서드가 충전된 PointModel을 반환하도록 수정하면 불필요한 데이터베이스 조회를 줄일 수 있습니다.

다음과 같이 개선할 수 있습니다:

PointService.java 수정:

 @Transactional
-public void charge(PointModel point) {
+public PointModel charge(PointModel point) {
     UserModel user = point.getUser();
     var foundUser = userRepository.findById(user.getId())
         .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "유저가 존재하지 않습니다."));
   
     var existing = pointRepository.findPoint(foundUser);
     if (existing.isPresent()) {
         existing.get().charge(point.getPoint());
-        pointRepository.save(existing.get());
-        return;
+        return pointRepository.save(existing.get());
     }
-    pointRepository.save(new PointModel(foundUser, point.getPoint()));
+    return pointRepository.save(new PointModel(foundUser, point.getPoint()));
 }

PointFacade.java 수정:

 public PointInfo chargePoint(UserId userId, Money point) {
     UserModel user = userService.getUser(userId);
     if (user == null) {
         throw new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 요청입니다.");
     }
     PointModel pointModel = new PointModel(user, point);
-    pointService.charge(pointModel);
-    
-    PointModel charged = pointService.findPoint(new PointModel(user, point));
+    PointModel charged = pointService.charge(pointModel);
     return PointInfo.from(charged);
 }
apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java (1)

48-54: 중복된 포인트 검증 조건을 정리해 주세요.
Line 48에서 이미 this.point.value() < usePoint.value() 조건으로 부족분을 차단하고 있어, Line 52의 usePoint.value() > this.point.value() 분기는 동일 조건을 다시 검사하는 unreachable 코드가 됩니다. 불필요한 분기는 유지보수 시 혼선을 줄 수 있으니 제거하는 편이 좋겠습니다.

-        if (usePoint.value() > this.point.value()) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "사용 금액이 보유 포인트를 초과합니다.");
-        }
apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java (1)

17-31: 헤더 파라미터 위치를 명시해 주세요.
@Parameter 기본값은 쿼리 파라미터로 문서화되기 때문에, 현재 상태로는 Swagger UI에서 X-USER-ID가 쿼리 파라미터로 노출됩니다. 실제 헤더 기반 호출과 문서가 어긋나 혼선을 줄 수 있으니 in = ParameterIn.HEADER(필요 시 ParameterIn import 추가) 또는 구현부와 동일하게 @RequestHeader를 붙여 명시적으로 표시해 주세요.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java (1)

17-39: 모든 UserId 파라미터에 헤더 위치를 지정해 주세요.
Point API와 동일하게 @Parameter만 사용하면 Swagger가 X-USER-ID를 쿼리 파라미터로 표기합니다. createOrdergetUserOrders의 UserId 모두 in = ParameterIn.HEADER(또는 @RequestHeader)를 지정해 실제 호출 방식과 문서가 일치하도록 보완해 주세요.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)

34-62: Switch 표현식을 사용한 리팩터링 고려

현재 switch 문은 Java 14+의 향상된 switch 표현식을 사용하여 더 간결하게 작성할 수 있습니다.

다음과 같이 리팩터링을 고려해보세요:

 private Pageable convertSortToPageable(String sort, Pageable pageable) {
-    Sort.Direction direction;
-    String property;
-    
-    switch (sort) {
-        case "latest":
-            property = "id";
-            direction = Sort.Direction.DESC;
-            break;
-        case "price_asc":
-            property = "price";
-            direction = Sort.Direction.ASC;
-            break;
-        case "likes_desc":
-            // likes_desc는 메모리에서 정렬하므로 여기서는 기본 정렬 사용
-            property = "id";
-            direction = Sort.Direction.DESC;
-            break;
-        default:
-            property = "id";
-            direction = Sort.Direction.DESC;
-    }
+    var sortConfig = switch (sort) {
+        case "price_asc" -> new Object() { String property = "price"; Sort.Direction direction = Sort.Direction.ASC; };
+        case "likes_desc", "latest", default -> new Object() { String property = "id"; Sort.Direction direction = Sort.Direction.DESC; };
+    };
     
     return org.springframework.data.domain.PageRequest.of(
         pageable.getPageNumber(),
         pageable.getPageSize(),
-        Sort.by(direction, property)
+        Sort.by(sortConfig.direction, sortConfig.property)
     );
 }
apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1)

17-20: 일관성 있는 에러 처리 패턴 고려

getUser 메서드가 사용자를 찾지 못했을 때 null을 반환하는 반면, signUp은 중복 시 예외를 던집니다. 호출하는 코드(UserFacade, PointFacade 등)에서 매번 null 체크 후 예외를 던지고 있으므로, 서비스 레이어에서 일관되게 예외를 던지는 것이 더 명확할 수 있습니다.

다음과 같이 리팩터링을 고려해보세요:

 @Transactional(readOnly = true)
 public UserModel getUser(UserId userId) {
-    return userRepository.find(userId).orElse(null);
+    return userRepository.find(userId)
+        .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 유저입니다."));
 }

이렇게 하면 파사드 레이어에서 null 체크를 제거할 수 있어 코드가 더 간결해집니다.

apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java (1)

35-38: 중복된 예외 처리 확인

ProductService.getQuantity()를 확인한 결과, 해당 메서드는 내부에서 이미 상품이 없을 때 NOT_FOUND 예외를 던지고 있습니다. 따라서 Line 37의 orElseThrow는 실행되지 않는 중복 코드입니다.

다음과 같이 단순화할 수 있습니다:

 public Quantity getQuantity(Long id) {
-    return productService.getQuantity(id).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품이 존재하지 않습니다."));
+    return productService.getQuantity(id).orElseThrow();
 }

또는 ProductService.getQuantity가 이미 예외를 던지므로 Optional을 반환하지 않고 Quantity를 직접 반환하도록 수정하는 것이 더 명확할 수 있습니다.

apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java (1)

28-34: 스트림 API 간소화 고려

Java 16 이상을 사용 중이라면, .collect(Collectors.toList()).toList()로 단순화할 수 있습니다.

다음과 같이 리팩터링할 수 있습니다:

 public List<ProductModel> findLikedProductsByUser(UserModel user) {
     return likeJpaRepository.findByUser(user).stream()
             .map(LikeModel::getProduct)
-            .collect(Collectors.toList());
+            .toList();
 }
apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java (1)

27-27: Void 응답에 null 대신 의미 있는 응답 고려.

ApiResponse.success(null)은 작동하지만, 성공 여부를 명시적으로 나타내는 응답 객체를 반환하는 것이 더 명확할 수 있습니다.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java (1)

17-26: 형식 검증 추가를 고려하세요.

필드의 존재 여부만 검증하고 있습니다. 다음 검증을 추가하는 것이 좋습니다:

  • email: @Email 어노테이션으로 이메일 형식 검증
  • gender: @Pattern 또는 enum으로 허용 값 제한
  • birthDate: @Pattern으로 날짜 형식 검증 (예: yyyy-MM-dd)

예시:

 public record SignupRequest(
     @NotBlank(message = "userId는 필수입니다.")
     String userId,
-    @NotBlank(message = "email은 필수입니다.")
+    @NotBlank(message = "email은 필수입니다.")
+    @Email(message = "올바른 이메일 형식이 아닙니다.")
     String email,
-    @NotBlank(message = "gender는 필수입니다.")
+    @NotBlank(message = "gender는 필수입니다.")
+    @Pattern(regexp = "^(MALE|FEMALE|OTHER)$", message = "올바른 성별 값이 아닙니다.")
     String gender,
-    @NotBlank(message = "birthDate는 필수입니다.")
+    @NotBlank(message = "birthDate는 필수입니다.")
+    @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "날짜 형식은 yyyy-MM-dd여야 합니다.")
     String birthDate
 ) {}
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java (2)

38-41: 좋아요 수 조회 로직 중복.

Lines 38-41과 Lines 81-83에서 동일한 패턴으로 좋아요 수를 조회하고 설정합니다. 이를 별도 메서드로 추출하여 중복을 제거하는 것이 좋습니다.

예시:

private void enrichProductsWithLikeCounts(List<ProductModel> products) {
    Map<Long, Long> likeCounts = likeRepository
        .countByProductIdsLiked(products.stream()
            .map(ProductModel::getId)
            .collect(Collectors.toSet()));
    products.forEach(product -> 
        product.setLikeCount(likeCounts.getOrDefault(product.getId(), 0L)));
}

Also applies to: 77-85


62-67: Optional 래핑이 불필요합니다.

메서드가 상품이 없으면 예외를 던지므로, Optional.of()로 래핑하는 것은 불필요합니다. 항상 값이 있는 경우에만 반환되므로 직접 Quantity를 반환하는 것이 더 명확합니다.

-@Transactional(readOnly = true)
-public Optional<Quantity> getQuantity(Long id) {
+@Transactional(readOnly = true)
+public Quantity getQuantity(Long id) {
     ProductModel product = productRepository.findById(id)
             .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품이 존재하지 않습니다."));
-    return Optional.of(product.getQuantity());
+    return product.getQuantity();
 }

이 경우 ProductFacade도 함께 수정이 필요합니다.

apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java (1)

72-72: OrderItemRequest에 검증 제약 조건 추가 권장.

레코드에 검증 어노테이션을 추가하면 API 레이어에서 더 일찍 오류를 잡을 수 있습니다.

-public record OrderItemRequest(Long productId, Integer quantity) {}
+public record OrderItemRequest(
+    @NotNull(message = "상품 ID는 필수입니다.")
+    Long productId,
+    @NotNull(message = "수량은 필수입니다.")
+    @Min(value = 1, message = "수량은 1개 이상이어야 합니다.")
+    Integer quantity
+) {}

필요한 import 추가:

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Min;
apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java (1)

42-44: OrderItemRequest에 검증 어노테이션 추가 고려.

현재 검증이 없어 null 또는 유효하지 않은 값이 서비스 계층까지 전달될 수 있습니다. OrderService.OrderItemRequest와 동일한 검증을 추가하는 것이 좋습니다.

 public record CreateOrderRequest(List<OrderItemRequest> items) {
-    public record OrderItemRequest(Long productId, Integer quantity) {}
+    public record OrderItemRequest(
+        @NotNull(message = "상품 ID는 필수입니다.")
+        Long productId,
+        @NotNull(message = "수량은 필수입니다.")
+        @Min(value = 1, message = "수량은 1개 이상이어야 합니다.")
+        Integer quantity
+    ) {}
 }

필요한 import 추가:

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Min;
apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java (1)

28-65: 검증 로직 중복을 제거하세요.

toggleLike, addLike, removeLike 메서드에서 사용자 및 상품 검증 로직이 동일하게 반복됩니다. 이를 별도의 private 메서드로 추출하여 중복을 제거하는 것이 좋습니다.

예시 리팩토링:

+   private UserModel validateAndGetUser(UserId userId) {
+      UserModel user = userService.getUser(userId);
+      if (user == null) {
+         throw new CoreException(ErrorType.NOT_FOUND, "유저를 찾을 수 없습니다.");
+      }
+      return user;
+   }
+
+   private ProductModel validateAndGetProduct(Long productId) {
+      ProductModel product = productService.getProduct(productId);
+      if (product == null) {
+         throw new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다.");
+      }
+      return product;
+   }

    @Transactional
    public void toggleLike(UserId userId, Long productId) {
-      UserModel user = userService.getUser(userId);
-      if (user == null) {
-         throw new CoreException(ErrorType.NOT_FOUND, "유저를 찾을 수 없습니다.");
-      }
-      ProductModel product = productService.getProduct(productId);
-      if (product == null) {
-         throw new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다.");
-      }
+      UserModel user = validateAndGetUser(userId);
+      ProductModel product = validateAndGetProduct(productId);
       likeService.toggleLike(user, product);
    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d9ae7ad and 27a4b1e.

📒 Files selected for processing (81)
  • .docs/design/01-requirements.md (1 hunks)
  • .docs/design/02-sequence-diagrams.md (1 hunks)
  • .docs/design/03-class-diagram.md (1 hunks)
  • .docs/design/04-erd.md (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/brand/BrandInfo.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/like/LikeInfo.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/common/Money.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/common/Quantity.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/like/LikeModel.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItemModel.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/order/OrderModel.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/product/Brand.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductModel.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/user/BirthDate.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/user/Email.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/user/Gender.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java (2 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandV1ApiSpec.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandV1Controller.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandV1Dto.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1ApiSpec.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Dto.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemModelTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/domain/order/OrderModelTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-10-31T14:57:09.157Z
Learnt from: minor7295
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 22
File: apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointsV1Controller.java:24-34
Timestamp: 2025-10-31T14:57:09.157Z
Learning: In the loopers-spring-java-template repository, the X-USER-ID header-based user identification is temporarily used for learning purposes and will be replaced with proper authentication (Spring Security) in a future PR. Do not flag this as a security issue until proper authentication is implemented.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java
  • apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java
📚 Learning: 2025-11-12T13:04:50.782Z
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 72
File: core/domain/src/main/java/com/loopers/core/domain/productlike/vo/ProductLikeId.java:5-7
Timestamp: 2025-11-12T13:04:50.782Z
Learning: In the com.loopers codebase, domain entity ID value objects (e.g., ProductLikeId, OrderItemId, ProductId, PaymentId, OrderId, BrandId) are system-generated identifiers and do not require pattern validation (regex, length checks). They are implemented as simple records with a String value and an empty() factory method returning null for unsaved entities. This differs from UserIdentifier, which is a user-supplied login ID that requires format validation. Domain IDs should not be flagged for missing validation logic in the create() method.
<!-- [add_learning]
UserIdentifier와 같은 사용자 입력 ID와 ProductLikeId, OrderItemId 등의 도메인 ID는 검증 패턴이 다릅니다. UserIdentifier는 사용자가 입력하는 로그인 ID로서 정규식, 길이 등의 형식 검증이 필요하지만, 도메인 ID는 시스템에서 생성하는 식별자(UUID, DB 생성 ID)이므로 패턴 검증이 불필요합니다. 도메인 ID VO는 단순한 record와 empty() 팩토리 메서드만으로 충분합니다.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java
  • apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java
  • apps/commerce-api/src/main/java/com/loopers/application/like/LikeInfo.java
  • apps/commerce-api/src/main/java/com/loopers/domain/user/Email.java
  • apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java
  • apps/commerce-api/src/main/java/com/loopers/domain/user/Gender.java
  • apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/domain/user/BirthDate.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/Brand.java
📚 Learning: 2025-10-31T02:20:33.781Z
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 15
File: core/domain/src/main/java/com/loopers/core/domain/user/vo/UserIdentifier.java:16-27
Timestamp: 2025-10-31T02:20:33.781Z
Learning: In UserIdentifier and similar value objects, when the constructor performs only null-checking while the static create() method performs full validation (regex, length, etc.), this is an intentional pattern for schema evolution. The constructor is used by the persistence layer to reconstruct domain objects from the database (no validation needed for already-validated legacy data), while create() is used by the application layer to create new domain objects (with validation for new data). This allows backward compatibility when validation rules change in production without requiring migration of all existing database records.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java
📚 Learning: 2025-11-09T10:41:39.297Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 25
File: apps/commerce-api/src/main/kotlin/com/loopers/domain/product/ProductRepository.kt:1-12
Timestamp: 2025-11-09T10:41:39.297Z
Learning: In this codebase, domain repository interfaces are allowed to use Spring Data's org.springframework.data.domain.Page and org.springframework.data.domain.Pageable types. This is an accepted architectural decision and should not be flagged as a DIP violation.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java
🧬 Code graph analysis (46)
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java (1)
  • RequiredArgsConstructor (21-86)
apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java (1)
  • RequiredArgsConstructor (14-39)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)
  • RequiredArgsConstructor (14-83)
apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java (2)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1)
  • RequiredArgsConstructor (11-31)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java (1)
  • RequiredArgsConstructor (13-50)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/user/BirthDate.java (1)
  • Embeddable (7-41)
apps/commerce-api/src/main/java/com/loopers/domain/user/Email.java (1)
  • Embeddable (7-41)
apps/commerce-api/src/main/java/com/loopers/domain/user/Gender.java (1)
  • Embeddable (7-38)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java (1)
  • Entity (8-49)
apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (1)
  • RequiredArgsConstructor (14-57)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (4)
apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (1)
  • RequiredArgsConstructor (14-46)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1)
  • RequiredArgsConstructor (11-31)
apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java (1)
  • RequiredArgsConstructor (11-30)
apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java (1)
  • RequiredArgsConstructor (11-25)
apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java (1)
  • RequiredArgsConstructor (12-79)
apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java (1)
  • RequiredArgsConstructor (18-90)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java (1)
  • RequiredArgsConstructor (13-49)
apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java (4)
apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java (1)
  • DisplayName (16-103)
apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java (3)
  • DisplayName (48-154)
  • DisplayName (156-182)
  • SpringBootTest (26-183)
apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemModelTest.java (1)
  • DisplayName (15-53)
apps/commerce-api/src/test/java/com/loopers/domain/order/OrderModelTest.java (2)
  • DisplayName (22-74)
  • DisplayName (76-126)
apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java (2)
apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java (1)
  • DisplayName (16-103)
apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java (2)
  • DisplayName (48-154)
  • DisplayName (156-182)
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductModel.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java (1)
  • Entity (15-60)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java (4)
apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java (1)
  • RequiredArgsConstructor (12-79)
apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java (1)
  • RequiredArgsConstructor (18-90)
apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java (1)
  • RequiredArgsConstructor (16-59)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Dto.java (1)
  • LikeV1Dto (8-37)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (4)
apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (1)
  • RequiredArgsConstructor (14-46)
apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java (1)
  • RequiredArgsConstructor (11-30)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (1)
  • RequiredArgsConstructor (14-57)
apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java (1)
  • RequiredArgsConstructor (14-32)
apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java (3)
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java (1)
  • RequiredArgsConstructor (14-43)
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java (1)
  • RequiredArgsConstructor (21-86)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)
  • RequiredArgsConstructor (14-83)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java (2)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java (1)
  • DisplayName (21-41)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java (3)
  • DisplayName (67-114)
  • DisplayName (116-167)
  • SpringBootTest (28-168)
apps/commerce-api/src/main/java/com/loopers/domain/user/Email.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/user/BirthDate.java (1)
  • Embeddable (7-41)
apps/commerce-api/src/main/java/com/loopers/domain/user/Gender.java (1)
  • Embeddable (7-38)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java (1)
  • Embeddable (7-40)
apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java (2)
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (1)
  • RequiredArgsConstructor (17-70)
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java (1)
  • RequiredArgsConstructor (18-73)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (3)
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java (1)
  • RequiredArgsConstructor (14-43)
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java (1)
  • RequiredArgsConstructor (21-86)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java (1)
  • ProductV1Dto (7-47)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java (2)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java (1)
  • PointV1Dto (6-19)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java (1)
  • SpringBootTest (30-165)
apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1)
  • RequiredArgsConstructor (11-31)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (1)
  • RequiredArgsConstructor (14-57)
apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java (1)
  • RequiredArgsConstructor (11-25)
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java (1)
  • RequiredArgsConstructor (18-73)
apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java (1)
  • RequiredArgsConstructor (12-32)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java (1)
  • RequiredArgsConstructor (17-59)
apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java (3)
apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java (1)
  • RequiredArgsConstructor (18-90)
apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java (1)
  • RequiredArgsConstructor (16-59)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java (1)
  • RequiredArgsConstructor (13-49)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java (2)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java (2)
  • DisplayName (48-86)
  • DisplayName (88-102)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java (2)
  • DisplayName (67-114)
  • DisplayName (116-167)
apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemModelTest.java (2)
apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java (1)
  • DisplayName (16-103)
apps/commerce-api/src/test/java/com/loopers/domain/order/OrderModelTest.java (2)
  • DisplayName (22-74)
  • DisplayName (76-126)
apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java (3)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java (1)
  • DisplayName (21-41)
apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java (2)
  • DisplayName (51-92)
  • DisplayName (94-128)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java (2)
  • DisplayName (66-117)
  • DisplayName (119-164)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java (1)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java (1)
  • UserV1Dto (6-27)
apps/commerce-api/src/main/java/com/loopers/domain/user/Gender.java (4)
apps/commerce-api/src/main/java/com/loopers/domain/product/Brand.java (1)
  • Embeddable (7-37)
apps/commerce-api/src/main/java/com/loopers/domain/user/BirthDate.java (1)
  • Embeddable (7-41)
apps/commerce-api/src/main/java/com/loopers/domain/user/Email.java (1)
  • Embeddable (7-41)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java (1)
  • Embeddable (7-40)
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItemModel.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderModel.java (1)
  • Entity (20-60)
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java (3)
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java (1)
  • RequiredArgsConstructor (14-43)
apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java (1)
  • RequiredArgsConstructor (14-39)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)
  • RequiredArgsConstructor (14-83)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java (4)
apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (1)
  • RequiredArgsConstructor (14-46)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (1)
  • RequiredArgsConstructor (14-57)
apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java (1)
  • RequiredArgsConstructor (14-32)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java (1)
  • PointV1Dto (6-19)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java (3)
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (1)
  • RequiredArgsConstructor (17-70)
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java (1)
  • RequiredArgsConstructor (18-73)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java (1)
  • OrderV1Dto (8-45)
apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java (3)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java (1)
  • DisplayName (21-41)
apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java (1)
  • DisplayName (20-40)
apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java (1)
  • DisplayName (58-234)
apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java (1)
  • RequiredArgsConstructor (12-79)
apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java (1)
  • RequiredArgsConstructor (16-59)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java (1)
  • RequiredArgsConstructor (13-49)
apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java (3)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java (1)
  • DisplayName (21-41)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java (3)
  • DisplayName (48-86)
  • DisplayName (88-102)
  • SpringBootTest (26-103)
apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java (3)
  • DisplayName (51-92)
  • DisplayName (94-128)
  • SpringBootTest (24-129)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandV1Controller.java (2)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java (1)
  • RequiredArgsConstructor (13-50)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandV1Dto.java (1)
  • BrandV1Dto (5-11)
apps/commerce-api/src/main/java/com/loopers/domain/user/BirthDate.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/user/Email.java (1)
  • Embeddable (7-41)
apps/commerce-api/src/main/java/com/loopers/domain/user/Gender.java (1)
  • Embeddable (7-38)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java (1)
  • Embeddable (7-40)
apps/commerce-api/src/main/java/com/loopers/domain/common/Money.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/common/Quantity.java (1)
  • Embeddable (7-37)
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderModel.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItemModel.java (1)
  • Entity (16-63)
apps/commerce-api/src/test/java/com/loopers/domain/order/OrderModelTest.java (3)
apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java (1)
  • DisplayName (16-103)
apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java (1)
  • DisplayName (20-40)
apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemModelTest.java (1)
  • DisplayName (15-53)
apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java (2)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java (3)
  • DisplayName (48-86)
  • DisplayName (88-102)
  • SpringBootTest (26-103)
apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java (1)
  • DisplayName (14-120)
apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (4)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1)
  • RequiredArgsConstructor (11-31)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java (1)
  • RequiredArgsConstructor (13-40)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (1)
  • RequiredArgsConstructor (14-57)
apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java (1)
  • RequiredArgsConstructor (11-25)
apps/commerce-api/src/main/java/com/loopers/domain/common/Quantity.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/common/Money.java (1)
  • Embeddable (7-37)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java (4)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1)
  • RequiredArgsConstructor (11-31)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java (1)
  • RequiredArgsConstructor (13-40)
apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java (1)
  • RequiredArgsConstructor (14-32)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java (1)
  • UserV1Dto (6-27)
apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java (2)
apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java (1)
  • DisplayName (20-40)
apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java (2)
  • DisplayName (58-234)
  • SpringBootTest (33-235)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java (1)
  • Entity (15-60)
apps/commerce-api/src/main/java/com/loopers/domain/product/Brand.java (6)
apps/commerce-api/src/main/java/com/loopers/domain/common/Money.java (1)
  • Embeddable (7-37)
apps/commerce-api/src/main/java/com/loopers/domain/common/Quantity.java (1)
  • Embeddable (7-37)
apps/commerce-api/src/main/java/com/loopers/domain/user/BirthDate.java (1)
  • Embeddable (7-41)
apps/commerce-api/src/main/java/com/loopers/domain/user/Email.java (1)
  • Embeddable (7-41)
apps/commerce-api/src/main/java/com/loopers/domain/user/Gender.java (1)
  • Embeddable (7-38)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java (1)
  • Embeddable (7-40)
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java (3)
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (1)
  • RequiredArgsConstructor (17-70)
apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java (1)
  • RequiredArgsConstructor (12-32)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java (1)
  • RequiredArgsConstructor (17-59)

Comment on lines 40 to 46
@DisplayName("상품 다건 조회 시 상품이 없으면 NOT_FOUND 예외가 발생한다.")
@Test
void productService_whenGetProductsIsNotFound() {
// arrange
productJpaRepository.save(new ProductModel("제목", new Brand("Apple"), new Money(10000), new Quantity(10)));

}
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

테스트가 불완전합니다.

arrange 단계에서 상품을 저장하지만, act/assert 단계가 없어 실제로 아무것도 검증하지 않습니다. DisplayName에 "상품이 없으면 NOT_FOUND 예외가 발생한다"고 명시되어 있으나 구현이 누락되었습니다.

테스트를 완성하거나 불필요하면 제거하세요:

         @DisplayName("상품 다건 조회 시 상품이 없으면 NOT_FOUND 예외가 발생한다.")
         @Test
         void productService_whenGetProductsIsNotFound() {
-            // arrange
-            productJpaRepository.save(new ProductModel("제목", new Brand("Apple"), new Money(10000), new Quantity(10)));
-
+            // act
+            CoreException result = assertThrows(CoreException.class, () -> {
+                productService.getProducts(/* appropriate parameters */);
+            });
+
+            // assert
+            assertThat(result.getErrorType()).isEqualTo(ErrorType.NOT_FOUND);
         }

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

🤖 Prompt for AI Agents
In
apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java
around lines 40 to 46, the test only contains arrange code and is missing the
act/assert steps; remove or adjust the saved product so the repository is empty
(or explicitly clear it), then call the service method that retrieves multiple
products (act) and assert that it throws the expected NOT_FOUND exception
(assert). Ensure you use the test framework's exception assertion (e.g.,
assertThrows) and include any necessary parameters for the service call to
reproduce the "no products" case.

@leeminkyu-kr96 leeminkyu-kr96 changed the title Feature/week3 set domain layer 상품, 브랜드, 좋아요, 주문 도메인 구현 Nov 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants