Skip to content

Conversation

@leeminkyu-kr96
Copy link
Collaborator

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

📌 Summary

2주차 과제인 이커머스 도메인(상품, 브랜드, 좋아요, 주문 등)에 대한 설계를 진행했습니다.
각 도메인 별 시나리오 정의서 , 요구사항명세서, 시퀀스 다이어그램, 클래스 다이어그램, ERD 작성하였습니다.

💬 Review Points

  1. 항상 설계 시 Main Flow에 대해서만 작성하고 Exception Flow에 대해서는 개발을 진행하며 예외처리를 했어서 유스케이스 작성 시 어려움이 있었습니다.
  2. 클래스 다이어그램과 ERD가 밀접하다고 생각이 들어 이거 수정하면 저거 수정하고 헤맸습니다. 특히 결제 부분에 있어서 고민을 너무 해서 오히려 생각이 안나서 결국 AI의 도움을 받았습니다..

✅ Checklist

  • 상품/브랜드/좋아요/주문 도메인이 모두 포함되어 있는가?
  • 기능 요구사항이 유저 중심으로 정리되어 있는가?
  • 시퀀스 다이어그램에서 책임 객체가 드러나는가?
  • 클래스 구조가 도메인 설계를 잘 표현하고 있는가?
  • ERD 설계 시 데이터 정합성을 고려하여 구성하였는가?

📎 References

Summary by CodeRabbit

릴리스 노트

새로운 기능

  • 사용자 회원가입 및 프로필 조회 기능 추가
  • 포인트 잔액 조회 기능 추가
  • 포인트 충전 기능 추가

문서

  • 제품 목록, 상세 조회, 브랜드 필터링 설계 문서 추가
  • 주문 생성 프로세스 설계 문서 추가
  • 데이터 모델 및 시스템 구조 설계 문서 추가

 - 개인 프로젝트에서 루퍼스프로젝트로 이전
 - 요구사항 정리 / 기능 명세
 - 시퀀스 다이어그램
 - 클래스 다이어그램
 - 도메인 모델 / 엔티티 / VO
 - ERD
@coderabbitai
Copy link

coderabbitai bot commented Nov 7, 2025

Walkthrough

사용자 및 포인트 관리 기능을 위한 전체 스택 구현이 추가되었습니다. 도메인 모델, 저장소, 서비스, 파사드, REST API 컨트롤러, DTO, 그리고 포괄적인 단위 및 통합 테스트가 포함됩니다. 설계 문서로는 요구사항, 시퀀스 다이어그램, 클래스 다이어그램, ERD가 제공됩니다.

Changes

응집도 / 파일 변경 요약
설계 문서
docs/design/01-requirements.md, docs/design/02-sequence-diagrams.md, docs/design/03-class-diagram.md, docs/design/04-erd.md
상품 목록/상세 조회, 브랜드 필터링, 좋아요, 주문 생성에 대한 요구사항과 Mermaid 시퀀스/클래스/ERD 다이어그램을 포함하는 설계 문서 4개 추가
사용자 도메인 계층
apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java, UserRepository.java, UserService.java
UserModel 엔티티(userId, email, birthDate 검증 포함), UserRepository 인터페이스, 가입 및 사용자 조회 기능을 하는 UserService 추가
사용자 응용 계층
apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java, UserInfo.java
signup 및 getUser 메서드를 제공하는 UserFacade, UserInfo 레코드 및 정적 매퍼 추가
사용자 저장소 구현
apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java, UserRepositoryImpl.java
Spring Data JPA UserJpaRepository 및 UserRepositoryImpl 구현체 추가
사용자 API 계층
apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java, UserV1Controller.java, UserV1Dto.java
signup/getUser 엔드포인트, UserV1Dto 레코드(UserResponse, SignupRequest) 및 유효성 검사 추가
포인트 도메인 계층
apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java, PointRepository.java, PointService.java
charge/use 메서드가 있는 PointModel 엔티티, PointRepository 인터페이스, 포인트 조회 및 충전 기능을 하는 PointService 추가
포인트 응용 계층
apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java, PointInfo.java
getPoint 및 chargePoint 메서드를 제공하는 PointFacade, PointInfo 레코드 및 정적 매퍼 추가
포인트 저장소 구현
apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java, PointRepositoryImpl.java
Spring Data JPA PointJpaRepository 및 PointRepositoryImpl 구현체 추가
포인트 API 계층
apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java, PointV1Controller.java, PointV1Dto.java
getPoint/chargePoint 엔드포인트, PointV1Dto 레코드(PointResponse, ChargeRequest) 및 유효성 검사 추가
API 예외 처리
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java
MissingRequestHeaderException 및 MethodArgumentNotValidException에 대한 두 개의 예외 핸들러 추가
사용자 테스트
apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java, UserServiceIntegrationTest.java, interfaces/api/UserV1ApiE2ETest.java
UserModel 검증, UserService 기능(가입, 조회), UserV1 API 엔드투엔드에 대한 단위/통합/E2E 테스트 추가
포인트 테스트
apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java, PointServiceIntegrationTest.java, interfaces/api/PointV1ApiE2ETest.java
PointModel 검증, PointService 기능(조회, 충전), PointV1 API 엔드투엔드에 대한 단위/통합/E2E 테스트 추가

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant UserController as UserV1Controller
    participant UserFacade
    participant UserService
    participant UserRepository
    participant UserDB as User DB
    participant PointFacade
    participant PointService
    participant PointRepository
    participant PointDB as Point DB

    rect rgb(200, 220, 240)
    Note over Client,UserController: 사용자 가입 (Signup)
    Client->>UserController: POST /api/v1/users/signup<br/>(userId, email, gender, birthDate)
    UserController->>UserFacade: signup(userId, email, birthDate)
    UserFacade->>UserService: signUp(UserModel)
    UserService->>UserRepository: find(userId)
    UserRepository->>UserDB: SELECT by userId
    alt User Exists
        UserService->>UserService: throw CONFLICT
    else User Not Exists
        UserService->>UserRepository: save(UserModel)
        UserRepository->>UserDB: INSERT
        UserDB-->>UserRepository: UserModel
        UserRepository-->>UserService: UserModel
    end
    UserService-->>UserFacade: UserModel
    UserFacade-->>UserController: UserInfo
    UserController-->>Client: 201 Created<br/>UserResponse
    end

    rect rgb(220, 240, 200)
    Note over Client,PointController: 포인트 충전 (Charge Points)
    Client->>UserController: POST /api/v1/points/charge<br/>X-USER-ID: userId<br/>(amount)
    UserController->>PointFacade: chargePoint(userId, amount)
    PointFacade->>UserService: getUser(userId)
    alt User Not Found
        UserService->>PointFacade: null
        PointFacade->>PointFacade: throw NOT_FOUND
    else User Found
        UserService-->>PointFacade: UserModel
        PointFacade->>PointService: charge(PointModel)
        PointService->>UserRepository: find(userId)
        UserRepository-->>PointService: UserModel
        rect rgb(255, 250, 200)
        Note over PointService,PointDB: Transaction
        PointService->>PointRepository: findPoint(UserModel)
        PointRepository->>PointDB: SELECT by user
        alt Point Exists
            PointDB-->>PointRepository: PointModel
            PointRepository-->>PointService: Optional(PointModel)
            PointService->>PointService: charge amount
            PointService->>PointRepository: save(PointModel)
        else Point Not Exists
            PointRepository-->>PointService: Optional.empty()
            PointService->>PointService: create new PointModel
            PointService->>PointRepository: save(PointModel)
        end
        PointRepository->>PointDB: INSERT/UPDATE
        PointDB-->>PointRepository: PointModel
        end
        PointRepository-->>PointService: PointModel
        PointService-->>PointFacade: void
        PointFacade->>PointRepository: findPoint(new PointModel(user, 0))
        PointRepository->>PointDB: SELECT by user
        PointDB-->>PointRepository: PointModel
        PointRepository-->>PointFacade: PointModel
        PointFacade-->>UserController: PointInfo
        UserController-->>Client: 200 OK<br/>PointResponse
    end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

주의가 필요한 영역:

  • PointModel과 UserModel의 검증 로직: 생성자 내 다중 검증 조건과 예외 처리 로직이 복잡하며, 특히 정규표현식 기반 검증이 의도대로 동작하는지 확인 필요
  • PointService와 UserService의 트랜잭션 처리: @Transactional 애너테이션 사용으로 인한 데이터 일관성, 특히 charge 메서드 내 create-or-update 로직의 정확성 검증 필요
  • PointFacade와 UserFacade의 에러 핸들링: 사용자 없음, 포인트 없음 등 예외 케이스 처리에서 일관성 있는 예외 전파 확인 필요
  • API 컨트롤러의 헤더 기반 사용자 식별: X-USER-ID 헤더 검증과 실제 사용자 존재 여부 확인 간 불일치 가능성 검토
  • DTOs의 유효성 검사: SignupRequest, ChargeRequest의 @NotBlank, @Min 애너테이션이 의도된 비즈니스 규칙을 정확히 반영하는지 확인
  • 테스트 커버리지: 정상/예외 시나리오가 모두 다루어지는지, 특히 동시성 상황에서의 포인트 충전 안정성 검토 필요

Possibly related PRs

Poem

🐰 사용자와 포인트, 우리가 담았고,
도메인 계층에서 데이터 지켜내고,
API 엔드포인트로 세상과 만나네!
검증과 테스트, 빈틈없이 다지고,
웹 상점의 꿈이 이제 시작이라 🌟

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.95% 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
Title check ✅ Passed 제목은 2주차 설계 문서 제출이라는 주요 변경사항을 명확하게 나타내고 있습니다.
Description check ✅ Passed PR 설명이 템플릿의 주요 섹션(Summary, Review Points, Checklist)을 모두 포함하고 있으며, 설계 과정에서의 고민 포인트와 체크리스트가 잘 작성되어 있습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@leeminkyu-kr96 leeminkyu-kr96 changed the title [2주차] 설계 문서 제출 - 이민 [2주차] 설계 문서 제출 - 이민규 Nov 7, 2025
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: 11

🧹 Nitpick comments (18)
.docs/design/04-erd.md (1)

1-48: ERD 구조는 합리적이나 네이밍 일관성 주의 필요.

ERD의 전체 구조와 관계 정의는 도메인 모델을 잘 반영합니다. 다만 클래스 다이어그램과의 네이밍 일관성을 확인하세요:

  • Products 테이블의 stock_quantity vs 클래스 다이어그램의 quantity
  • Orders 테이블의 created_at vs 클래스 다이어그램의 orderDate

또한 데이터 타입 관점에서 int 타입 사용(price, point 등)이 대규모 거래량을 감당할 수 있는지 검토하는 것을 권장합니다(오버플로우 위험).

.docs/design/02-sequence-diagrams.md (1)

30-64: 주문 생성 시퀀스에서 예외 흐름 상세화 권장.

주문 생성 시퀀스 다이어그램의 TransactionBlock은 적절하게 설계되었습니다. 다만 다음 예외 상황에 대한 흐름을 명시적으로 추가하면 더욱 명확해질 것입니다:

  • 재고 부족 시 흐름 (라인 55에서 실패한 경우)
  • 포인트 부족 시 흐름 (라인 56에서 실패한 경우)
  • 트랜잭션 롤백 시 응답 방식

현재는 성공 경로만 명시되어 있으므로, 요구사항 명세(01-requirements.md)의 Exception Flow들을 시퀀스 다이어그램에도 반영하면 설계의 완전성이 향상될 것입니다.

.docs/design/01-requirements.md (1)

1-137: 설계 문서 전체: 구조와 내용 종합 평가.

요구사항 명세서의 전체 구조는 우수하며, 사용자 시나리오 기반의 기능 정의와 Main/Alternate/Exception Flow 분류가 체계적입니다. 특히 Exception Flow가 명확하게 정의된 점은 좋습니다(재고 부족, 포인트 부족, 비회원 등).

다음 항목들을 최종 검토하세요:

  1. 네이밍 일관성: ERD, 클래스 다이어그램, 시퀀스 다이어그램, 요구사항 간 필드명 통일

    • quantity vs stock_quantity
    • orderDate vs created_at
  2. 설계 상세도: 시퀀스 다이어그램에 Exception Flow와 트랜잭션 롤백 시나리오 명시화

  3. 데이터 타입 검토: int 타입의 가격/포인트 필드에 대한 overflow 고려

제출된 설계 문서들은 전체적으로 일관성 있게 작성되었으며, 요구사항을 반영한 도메인 모델링이 적절합니다.

apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java (1)

6-7: find 메서드를 findByUserId로 명확히 해주세요.
도메인 인터페이스에서 find(String userId)라는 이름은 조회 기준이 무엇인지 불분명해 오용 가능성이 큽니다. findByUserId처럼 의도를 드러내도록 이름을 조정하면 가독성과 유지보수성이 좋아집니다.

필요한 경우 아래처럼 정리해 주세요:

-    Optional<UserModel> find(String userId);
+    Optional<UserModel> findByUserId(String userId);
apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java (1)

6-12: 애플리케이션 계층에서 도메인 엔티티 노출을 피하는 편이 좋습니다.
현재 PointInfoUserModel을 그대로 담고 있어 애플리케이션 계층에서 도메인 엔티티가 외부로 새는 구조입니다. UserInfo로 변환해 전달하면 계층 간 결합도가 낮아지고 이후 API DTO 매핑도 더 명확해집니다.

아래와 같이 조정해 보세요:

-import com.loopers.domain.user.UserModel;
+import com.loopers.application.user.UserInfo;
 
-public record PointInfo(Long id, UserModel user, int point) {
+public record PointInfo(Long id, UserInfo user, int point) {
     public static PointInfo from(PointModel model) {
-        return new PointInfo(model.getId(), model.getUser(), model.getPoint());
+        return new PointInfo(
+            model.getId(),
+            UserInfo.from(model.getUser()),
+            model.getPoint()
+        );
     }
apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java (1)

17-26: 이메일과 생년월일에 대한 형식 검증 추가를 고려하세요.

현재 @NotBlank 검증만 있어서 빈 값은 방지하지만, 잘못된 형식의 데이터는 허용됩니다. 이메일 형식과 날짜 형식에 대한 추가 검증을 고려해보세요.

다음과 같이 검증을 추가할 수 있습니다:

 public record SignupRequest(
     @NotBlank(message = "userId는 필수입니다.")
     String userId,
-    @NotBlank(message = "email은 필수입니다.")
+    @NotBlank(message = "email은 필수입니다.")
+    @Email(message = "유효한 이메일 형식이 아닙니다.")
     String email,
     @NotBlank(message = "gender는 필수입니다.")
     String gender,
-    @NotBlank(message = "birthDate는 필수입니다.")
+    @NotBlank(message = "birthDate는 필수입니다.")
+    @Pattern(regexp = "\\d{4}-\\d{2}-\\d{2}", message = "생년월일은 yyyy-MM-dd 형식이어야 합니다.")
     String birthDate
 ) {}

@Email 사용을 위해 import 추가:

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Pattern;
apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java (1)

7-10: LGTM - 메서드 네이밍 개선 제안.

인터페이스 정의는 깔끔합니다. 다만 findPoint(UserModel user) 메서드명을 findByUser(UserModel user)로 변경하면 JPA 컨벤션과 더 일관성을 가질 수 있습니다. PointJpaRepository에서 이미 findByUser를 사용하고 있으므로 도메인 레이어에서도 동일한 네이밍을 사용하는 것이 좋습니다.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java (1)

16-19: primitive int 대신 Integer 래퍼 클래스 사용을 고려하세요.

primitive int는 JSON 요청에서 amount 필드가 누락되면 기본값 0으로 설정됩니다. 이는 @Min(1) 검증을 우회하지는 못하지만, 필드 누락과 명시적인 0 값을 구분할 수 없게 만듭니다. Integer 래퍼 클래스를 사용하면 @NotNull 검증을 추가하여 더 명확한 에러 메시지를 제공할 수 있습니다.

다음과 같이 수정할 수 있습니다:

 public record ChargeRequest(
+    @NotNull(message = "포인트 금액은 필수입니다.")
-    @Min(value = 1, message = "포인트는 1 이상이어야 합니다.")
+    @Min(value = 1, message = "포인트는 1 이상이어야 합니다.")
-    int amount
+    Integer amount
 ) {}
apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java (1)

48-63: PointService.findPoint API 사용이 혼란스럽습니다.

Line 56에서 pointService.findPoint(pointModel)을 호출할 때 amount가 10인 PointModel을 전달하지만, 실제로는 user 정보만 사용됩니다. PointService.findPoint는 PointModel을 받지만 내부적으로는 user만 사용하므로 혼란을 줄 수 있습니다.

PointService.findPoint 메서드 시그니처를 다음과 같이 변경하는 것을 고려하세요:

// PointService.java
public PointModel findPoint(UserModel user) {
    var foundUser = userRepository.find(user.getUserId());
    if (foundUser.isEmpty()) {
        return null;
    }
    return pointRepository.findPoint(foundUser.get()).orElse(null);
}

그러면 테스트 코드도 더 명확해집니다:

PointModel result = pointService.findPoint(user);
apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (2)

19-27: 메서드 시그니처와 중복 조회를 개선하세요.

findPoint 메서드가 PointModel을 파라미터로 받지만 실제로는 user 정보만 사용하고 있습니다. 또한, 이미 PointModel에 포함된 사용자를 다시 UserRepository로 조회하고 있어 중복 조회가 발생합니다.

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

-    @Transactional(readOnly = true)
-    public PointModel findPoint(PointModel point) {
-        UserModel requestUser = point.getUser();
-        var foundUser = userRepository.find(requestUser.getUserId());
-        if (foundUser.isEmpty()) {
-            return null;
-        }
-        return pointRepository.findPoint(foundUser.get()).orElse(null);
-    }
+    @Transactional(readOnly = true)
+    public PointModel findPoint(UserModel user) {
+        return pointRepository.findPoint(user).orElse(null);
+    }

29-42: 일관성 없는 에러 처리를 개선하세요.

charge 메서드는 사용자가 없을 때 예외를 던지지만, findPoint 메서드는 null을 반환합니다. 또한 charge 메서드도 PointModel 전체를 받지만 실제로는 user와 point 금액만 필요합니다. PointFacade에서 이미 사용자 존재 여부를 확인하므로 여기서 다시 조회하는 것도 중복입니다.

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

     @Transactional
-    public void charge(PointModel point) {
-        UserModel user = point.getUser();
-        var foundUser = userRepository.find(user.getUserId())
-            .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "유저가 존재하지 않습니다."));
-  
-        var existing = pointRepository.findPoint(foundUser);
+    public PointModel charge(UserModel user, int amount) {
+        var existing = pointRepository.findPoint(user);
         if (existing.isPresent()) {
-            existing.get().charge(point.getPoint());
-            pointRepository.save(existing.get());
-            return;
+            PointModel existingPoint = existing.get();
+            existingPoint.charge(amount);
+            return pointRepository.save(existingPoint);
         }
-        pointRepository.save(new PointModel(foundUser, point.getPoint()));
+        return pointRepository.save(new PointModel(user, amount));
     }
apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1)

17-20: 일관성 없는 에러 처리 방식을 개선하세요.

getUser 메서드는 사용자를 찾지 못하면 null을 반환하지만, signUp 메서드는 중복 시 예외를 던집니다. 이로 인해 Facade 레이어에서 매번 null 체크를 해야 합니다 (PointFacade.java 19-22줄, 34-37줄, UserFacade.java 18-21줄 참조).

다음과 같이 Optional을 반환하도록 개선할 수 있습니다:

      @Transactional(readOnly = true)
-     public UserModel getUser(String userId) {
-         return userRepository.find(userId).orElse(null);
+     public Optional<UserModel> getUser(String userId) {
+         return userRepository.find(userId);
      }
apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java (1)

91-112: 테스트 설명이 구체적이지 않습니다.

테스트 이름이 "회원 가입 시에 성별이 없을 경우"이지만 실제로는 gender 필드를 null로 설정하여 @notblank 검증을 테스트하고 있습니다. 테스트가 검증하는 내용을 더 명확하게 표현하면 좋겠습니다.

-        @DisplayName("회원 가입 시에 성별이 없을 경우, 400 Bad Request 응답을 반환한다.")
+        @DisplayName("회원 가입 시에 필수 필드(gender)가 null일 경우, 400 Bad Request 응답을 반환한다.")
apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java (2)

20-23: 스텁 구현이 있습니다.

findById 메서드가 항상 Optional.empty()를 반환하는 스텁 구현으로 남아있습니다. 현재 사용되지 않는다면 문제없지만, 실수로 호출될 경우 예상치 못한 동작이 발생할 수 있습니다.

구현이 필요 없다면 명시적으로 UnsupportedOperationException을 던지는 것을 고려해보세요:

     @Override
     public Optional<UserModel> findById(Long id) {
-        return Optional.empty();
+        throw new UnsupportedOperationException("findById is not implemented yet");
     }

30-31: 불필요한 빈 줄을 제거하세요.

파일 끝에 불필요한 빈 줄이 있습니다.

apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (2)

18-31: 중복된 null 체크와 비효율적인 객체 생성이 있습니다.

  1. UserService.getUser에서 사용자를 조회한 후 null 체크를 하고, PointService.findPoint 내부에서도 다시 사용자 조회를 합니다 (PointService.java 21-26줄).
  2. 조회를 위해 new PointModel(user, 0)으로 불필요한 객체를 생성하고 있습니다.

PointService의 메서드 시그니처를 개선하면 이 문제들을 해결할 수 있습니다:

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

33-43: 충전 후 재조회를 제거하세요.

pointService.charge 호출 후 업데이트된 포인트를 다시 조회하고 있습니다 (line 41). 이는 불필요한 DB 조회입니다. PointService.charge가 업데이트된 PointModel을 반환하도록 개선하면 재조회를 피할 수 있습니다.

PointService.charge가 PointModel을 반환하도록 수정하면:

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

20-20: 헤더 이름을 상수로 추출하는 것을 권장합니다.

"X-USER-ID" 헤더 이름이 하드코딩되어 있어 유지보수성이 떨어집니다. 두 엔드포인트에서 동일한 헤더를 사용하므로, 상수로 추출하여 일관성을 유지하는 것이 좋습니다.

다음과 같이 상수를 정의할 수 있습니다:

 @RequiredArgsConstructor
 @RestController
 @RequestMapping("/api/v1/points")
 public class PointV1Controller implements PointV1ApiSpec {
+    private static final String USER_ID_HEADER = "X-USER-ID";
 
     private final PointFacade pointFacade;
 
     @GetMapping
     @Override
     public ApiResponse<PointV1Dto.PointResponse> getPoint(
-        @RequestHeader(value = "X-USER-ID") String userId
+        @RequestHeader(value = USER_ID_HEADER) String userId
     ) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d5ec397 and 5974fe4.

📒 Files selected for processing (31)
  • .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/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/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/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/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/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/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/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/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/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/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 (2)
📚 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/infrastructure/user/UserJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.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/test/java/com/loopers/domain/user/UserModelTest.java
🧬 Code graph analysis (16)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (3)
apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (1)
  • RequiredArgsConstructor (12-44)
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/user/UserRepositoryImpl.java (1)
  • RequiredArgsConstructor (10-32)
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 (43-81)
  • DisplayName (83-97)
  • SpringBootTest (21-98)
apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java (1)
  • DisplayName (14-122)
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-20)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java (1)
  • SpringBootTest (26-161)
apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.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/infrastructure/user/UserRepositoryImpl.java (1)
  • RequiredArgsConstructor (10-32)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java (1)
  • RequiredArgsConstructor (11-38)
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 (12-44)
apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java (1)
  • RequiredArgsConstructor (10-28)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (1)
  • RequiredArgsConstructor (12-43)
apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java (1)
  • RequiredArgsConstructor (10-32)
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 (9-59)
apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java (2)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (1)
  • RequiredArgsConstructor (12-43)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1)
  • RequiredArgsConstructor (11-31)
apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (2)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (1)
  • RequiredArgsConstructor (12-43)
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/point/PointRepositoryImpl.java (3)
apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (1)
  • RequiredArgsConstructor (12-44)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (1)
  • RequiredArgsConstructor (12-43)
apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java (1)
  • RequiredArgsConstructor (10-32)
apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java (2)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java (1)
  • DisplayName (16-36)
apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java (2)
  • DisplayName (52-93)
  • DisplayName (95-129)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java (3)
apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java (1)
  • RequiredArgsConstructor (10-28)
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/UserV1Dto.java (1)
  • UserV1Dto (6-27)
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 (43-81)
  • DisplayName (83-97)
apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java (1)
  • DisplayName (14-122)
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 (12-57)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java (2)
apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (1)
  • RequiredArgsConstructor (12-44)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java (1)
  • PointV1Dto (6-20)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java (3)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java (1)
  • DisplayName (16-36)
apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java (3)
  • DisplayName (52-93)
  • DisplayName (95-129)
  • SpringBootTest (25-130)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java (3)
  • DisplayName (62-109)
  • DisplayName (111-162)
  • SpringBootTest (23-163)
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)
🔇 Additional comments (17)
apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java (2)

15-19: 회원가입 로직은 정상적으로 동작합니다.

UserModel 생성 및 저장 로직이 명확합니다. 다만 SignupRequest의 gender 필드가 사용되지 않는 점은 UserV1Dto.java에서 언급했습니다.


21-27: LGTM - 적절한 예외 처리.

사용자가 존재하지 않을 때 명확한 에러 메시지와 함께 CoreException을 발생시키는 것이 좋습니다.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java (1)

7-14: LGTM - PointResponse 매핑 로직.

PointInfo에서 PointResponse로의 변환이 명확하게 구현되어 있습니다.

apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java (1)

86-95: LGTM - 유효하지 않은 사용자 예외 처리 테스트.

존재하지 않는 사용자로 포인트 충전 시 CoreException이 발생하는지 확인하는 테스트가 적절합니다.

apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java (1)

11-25: LGTM - 깔끔한 Repository 구현.

PointJpaRepository로의 위임이 명확하고 간결합니다. 도메인 레이어와 인프라 레이어 분리가 잘 되어 있습니다.

apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java (5)

23-50: LGTM - 적절한 E2E 테스트 구성.

SpringBootTest 설정과 테스트 픽스처 정리가 잘 되어 있습니다. 각 테스트 후 데이터베이스를 정리하는 것이 좋습니다.


67-89: LGTM - 포인트 조회 성공 시나리오 테스트.

사용자와 포인트를 생성하고 X-USER-ID 헤더로 조회하는 전체 흐름이 잘 테스트되어 있습니다. assertAll을 사용한 다중 검증도 적절합니다.


93-108: LGTM - 헤더 누락 시나리오 테스트.

필수 헤더가 없을 때 400 Bad Request를 반환하는지 확인하는 테스트가 좋습니다.


116-139: LGTM - 포인트 충전 성공 시나리오 테스트.

1000 포인트 충전 후 응답 검증이 체계적으로 이루어지고 있습니다.


143-161: LGTM - 존재하지 않는 사용자 예외 테스트.

존재하지 않는 사용자로 충전 시도 시 404 Not Found를 반환하는지 확인하는 테스트가 적절합니다.

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

8-28: LGTM - 명확한 OpenAPI 스펙 정의.

User V1 API의 계약이 OpenAPI 어노테이션으로 잘 정의되어 있습니다. 한글로 작성된 설명도 명확합니다.

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

52-65: 잘 구현되었습니다!

새로 추가된 예외 핸들러들이 기존 패턴을 잘 따르고 있으며, 적절한 에러 메시지를 제공합니다. 헤더 누락과 검증 실패에 대한 처리가 명확합니다.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java (1)

9-31: 잘 작성된 API 명세입니다!

Swagger 어노테이션이 적절하게 사용되었고, API 문서가 명확합니다. 인터페이스 기반 설계로 관심사 분리도 잘 되어 있습니다.

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

29-37: 잘 구현되었습니다!

사용자 조회 엔드포인트가 명확하고 간결하게 구현되었습니다.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java (3)

1-8: LGTM!

필요한 의존성들이 적절하게 import되어 있습니다.


10-15: LGTM!

파사드 패턴을 사용한 깔끔한 컨트롤러 구조입니다. 의존성 주입도 적절합니다.


27-36: 요청 본문 검증은 잘 되어 있습니다.

@Valid 어노테이션을 통해 ChargeRequest의 검증이 제대로 수행되고 있습니다. 다만, Line 20에서 언급한 X-USER-ID 헤더 검증 이슈가 이 메서드에도 동일하게 적용됩니다.

### 2.3. 유스케이스 흐름
* **Main Flow (상세 조회):**
1. 사용자가 상품 목록에서 특정 상품을 클릭한다.
2. 해당 상푸의 상세 정보(이름, 가격, 설명, 총 좋아요 수, 나의 좋아요 여부 등)를 조회하여 반환한다.
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

타이포 수정: "상푸" → "상품".

-    2.  해당 상푸의 상세 정보(이름, 가격, 설명, 총 좋아요 수, 나의 좋아요 여부 등)를 조회하여 반환한다.
+    2.  해당 상품의 상세 정보(이름, 가격, 설명, 총 좋아요 수, 나의 좋아요 여부 등)를 조회하여 반환한다.
📝 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
2. 해당 상푸의 상세 정보(이름, 가격, 설명, 총 좋아요 수, 나의 좋아요 여부 등)를 조회하여 반환한다.
2. 해당 상품의 상세 정보(이름, 가격, 설명, 총 좋아요 수, 나의 좋아요 여부 등)를 조회하여 반환한다.
🤖 Prompt for AI Agents
In .docs/design/01-requirements.md around line 46, there is a typo "상푸" that
should be corrected to "상품"; update the text to read "해당 상품의 상세 정보(이름, 가격, 설명, 총
좋아요 수, 나의 좋아요 여부 등)를 조회하여 반환한다." ensuring only the single word is fixed and
punctuation/spacing remains unchanged.

Comment on lines +70 to +71
1. 사용자가 'A 브랜드'를 조회회한다.
2. 해당 브랜드드 상품 목록을 반환한다.
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

타이포 수정: 문법 오류 2건.

-    1.  사용자가 'A 브랜드'를 조회회한다.
-    2.  해당 브랜드드 상품 목록을 반환한다.
+    1.  사용자가 'A 브랜드'를 조회한다.
+    2.  해당 브랜드의 상품 목록을 반환한다.
📝 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
1. 사용자가 'A 브랜드'를 조회회한다.
2. 해당 브랜드드 상품 목록을 반환한다.
1. 사용자가 'A 브랜드'를 조회한다.
2. 해당 브랜드의 상품 목록을 반환한다.
🤖 Prompt for AI Agents
In .docs/design/01-requirements.md around lines 70 to 71, fix two typos: change
"조회회한다" to "조회한다" and change "해당 브랜드드 상품 목록을 반환한다." to "해당 브랜드의 상품 목록을 반환한다."

Comment on lines +10 to +15
class Product {
Long id
String name
int price
int quantity
}
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

🧩 Analysis chain

필드명 일관성 검토: ERD와 클래스 다이어그램 동기화 필요.

Product 클래스의 필드명이 ERD와 다릅니다:

  • 클래스 다이어그램: quantity
  • ERD: stock_quantity

요구사항 및 시퀀스 다이어그램에서 사용할 정확한 필드명을 결정하고 전체 설계 문서에서 일관되게 사용하세요.



---

🏁 Script executed:

```shell
#!/bin/bash
# Find all design documents in the .docs/design directory
find .docs/design -type f \( -name "*.md" -o -name "*.mmd" -o -name "*.txt" \) | sort

Length of output: 124


🏁 Script executed:

#!/bin/bash
# Search for ERD-related files
fd -e md -e mmd -e txt . .docs/design | grep -i erd

Length of output: 22


🏁 Script executed:

#!/bin/bash
# Examine the class diagram file to see Product class definition
cat ".docs/design/03-class-diagram.md"

Length of output: 838


🏁 Script executed:

#!/bin/bash
# Search for field name references (quantity, stock_quantity, stock) in all design docs
rg -n "(quantity|stock_quantity|stock)" .docs/design --type md

Length of output: 677


🏁 Script executed:

#!/bin/bash
# List all files in .docs/design to understand document structure
ls -la .docs/design/

Length of output: 380



---

🏁 Script executed:

```shell
#!/bin/bash
# Examine the ERD file completely to see Product entity field names
cat ".docs/design/04-erd.md"

Length of output: 918


🏁 Script executed:

#!/bin/bash
# Examine requirements to see specified field names
cat ".docs/design/01-requirements.md"

Length of output: 3680


🏁 Script executed:

#!/bin/bash
# Examine sequence diagrams for context
cat ".docs/design/02-sequence-diagrams.md"

Length of output: 1977


ERD와 클래스 다이어그램의 필드명 불일치 수정 필요

검증 결과, 설계 문서 간 필드명 불일치가 확인되었습니다:

  • ERD (04-erd.md 라인 14): products 테이블에 stock_quantity 정의
  • 클래스 다이어그램 (03-class-diagram.md 라인 14): Product 클래스에 quantity 정의

orderitemsquantity(주문 수량)와 구분하기 위해, Product의 재고 필드명을 명확히 결정하고 ERD, 클래스 다이어그램, 요구사항, 시퀀스 다이어그램 전체에서 일관되게 적용하세요.

🤖 Prompt for AI Agents
In .docs/design/03-class-diagram.md around lines 10 to 15, the Product class
uses the field name "quantity" which conflicts with the ERD's "stock_quantity"
and can be confused with orderitems.quantity; rename Product.quantity to
stock_quantity in this file and update the wording to match the ERD, then review
and make the same rename across related artifacts (04-erd.md, requirements,
sequence diagrams, and any references in docs/tests) so all diagrams and docs
use "stock_quantity" consistently.

Comment on lines +24 to +29
class Order {
Long id
User user
int totalPrice
Timestamp orderDate
}
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

🧩 Analysis chain

필드명 일관성 검토: Order 클래스의 시간 필드.

Order 클래스의 필드명이 ERD와 다릅니다:

  • 클래스 다이어그램: orderDate
  • ERD: created_at

데이터베이스 스키마와 도메인 모델의 네이밍을 통일하고, 시퀀스 다이어그램과도 일관성 있게 유지하세요.


🏁 Script executed:

#!/bin/bash
# List all files in .docs/design directory to identify ERD and sequence diagram files
find .docs/design -type f -name "*.md" | sort

Length of output: 124


🏁 Script executed:

#!/bin/bash
# Search for ERD-related content showing database field names
fd -e md . .docs/design -x cat {}

Length of output: 7413


🏁 Script executed:

#!/bin/bash
# Search for "created_at" or "orderDate" references in design docs
rg -n -i "(created_at|orderDate)" .docs/design/

Length of output: 118


🏁 Script executed:

#!/bin/bash
# Look for Order class definitions in the codebase
rg -n -C5 "class Order" --type=java --type=ts --type=js --type=py

Length of output: 0


Order 클래스 시간 필드 네이밍 불일치 확인됨 - 수정 필요

검증 결과 설계 문서에서 다음과 같은 불일치가 확인되었습니다:

  • ERD (.docs/design/04-erd.md, 29줄): created_at
  • 클래스 다이어그램 (.docs/design/03-class-diagram.md, 28줄): orderDate

데이터베이스 스키마(snake_case)와 도메인 모델(camelCase) 간 명확한 네이밍 규칙을 수립하거나, 둘 중 하나로 통일하세요. 다른 엔티티(User, Product 등)의 시간 필드도 동일한 원칙으로 일관성 있게 정의되어야 합니다.

🤖 Prompt for AI Agents
In .docs/design/03-class-diagram.md around lines 24–29, the Order class uses
"orderDate" which conflicts with the ERD (.docs/design/04-erd.md line 29) that
defines the timestamp as "created_at"; decide on a consistent convention (either
DB snake_case and domain camelCase mapping rule, or unify both to one style) and
apply it: update the class diagram to use the chosen name (e.g., change
orderDate → createdAt if you keep domain camelCase, or → created_at if you want
exact DB names), then update the ERD or other class diagrams (User, Product,
etc.) accordingly to reflect the same rule and add a brief note in the design
docs describing the naming convention for timestamp fields.

OrderItem --> Product : (주문 항목은 상품을 가짐)

Like --> User : (좋아요는 유저를 가짐)
Like -- > Product : (좋아요는 상품을 가짐) No newline at end of file
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Mermaid 문법 오류: 관계 정의 수정 필요.

라인 46에서 관계 정의에 오타가 있습니다. 스페이스가 불필요하게 포함되어 있습니다.

-    Like -- > Product : (좋아요는 상품을 가짐)
+    Like --> Product : (좋아요는 상품을 가짐)
📝 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
Like -- > Product : (좋아요는 상품을 가짐)
Like --> Product : (좋아요는 상품을 가짐)
🤖 Prompt for AI Agents
.docs/design/03-class-diagram.md around line 46: the Mermaid relationship has an
extra space in "Like -- > Product : (좋아요는 상품을 가짐)"; remove the space so the
arrow is "Like --> Product : (좋아요는 상품을 가짐)" to follow correct Mermaid syntax.

Comment on lines +47 to +56
public void use(int amount) {

if (amount < 0) {
throw new CoreException(ErrorType.BAD_REQUEST, "사용 금액은 0보다 커야 합니다.");
}
if (point < 0) {
throw new CoreException(ErrorType.BAD_REQUEST, "포인트는 0 이상이어야 합니다.");
}
this.point -= amount;
}
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

사용 로직에서 잔액 부족 검증 누락

Line 47~56에서 point가 충분한지 확인하지 않고 차감하고 있어, point=10 상태에서 amount=20을 호출하면 잔액이 -10까지 내려갑니다. 이는 포인트 도메인의 핵심 불변식을 깨뜨리는 치명적 버그입니다. 차감 전에 보유 포인트 이상인지 검증해 주세요.

     public void use(int amount) {
 
         if (amount < 0) {
             throw new CoreException(ErrorType.BAD_REQUEST, "사용 금액은 0보다 커야 합니다.");
         }
-        if (point < 0) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "포인트는 0 이상이어야 합니다.");
-        }
-        this.point -= amount;
+        if (amount > this.point) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "사용 금액이 보유 포인트를 초과합니다.");
+        }
+        this.point -= amount;
     }
🤖 Prompt for AI Agents
In apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java
around lines 47-56, the use(int amount) method currently decrements point
without verifying sufficiency; add a validation after the existing non-negative
checks that throws a CoreException(ErrorType.BAD_REQUEST, "보유 포인트가 부족합니다.") (or
similar message) when amount > point, and only subtract amount from point when
the check passes to prevent negative balances.

Comment on lines +19 to +21
public ApiResponse<PointV1Dto.PointResponse> getPoint(
@RequestHeader(value = "X-USER-ID") String userId
) {
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

X-USER-ID 헤더 값 검증 추가가 필요합니다.

현재 @RequestHeader는 헤더의 존재 여부만 확인하고, 빈 문자열이나 공백 문자열에 대한 검증은 수행하지 않습니다. 빈 userId가 파사드로 전달되면 혼란스러운 에러 메시지가 발생할 수 있습니다.

헤더 값 검증을 추가하거나, 파사드 레벨에서 명시적인 검증을 수행하는 것을 권장합니다.

다음과 같이 수정할 수 있습니다:

     public ApiResponse<PointV1Dto.PointResponse> getPoint(
-        @RequestHeader(value = "X-USER-ID") String userId
+        @RequestHeader(value = "X-USER-ID") @NotBlank(message = "사용자 ID는 필수입니다.") String userId
     ) {

또는 컨트롤러 메서드 내에서 검증:

     public ApiResponse<PointV1Dto.PointResponse> getPoint(
         @RequestHeader(value = "X-USER-ID") String userId
     ) {
+        if (userId == null || userId.isBlank()) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "사용자 ID는 필수입니다.");
+        }
         PointInfo info = pointFacade.getPoint(userId);

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

🤖 Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java
around lines 19-21, the X-USER-ID header is only checked for presence but not
for empty or blank values; add validation so empty/whitespace-only userId is
rejected. Fix by enforcing non-blank header either via annotation (add
@Validated on the controller and change the parameter to
@RequestHeader("X-USER-ID") @NotBlank String userId) or by explicit check at
method start (if userId == null || userId.trim().isEmpty() throw a 400
BadRequest with a clear message), and ensure the controller returns a proper 400
response rather than forwarding an empty userId to the facade.

Comment on lines +18 to +27
@PostMapping("/signup")
@ResponseStatus(HttpStatus.CREATED)
@Override
public ApiResponse<UserV1Dto.UserResponse> signup(
@Valid @RequestBody UserV1Dto.SignupRequest request
) {
UserInfo info = userFacade.signup(request.userId(), request.email(), request.birthDate());
UserV1Dto.UserResponse response = UserV1Dto.UserResponse.from(info);
return ApiResponse.success(response);
}
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

gender 필드가 누락되어 데이터가 손실됩니다.

SignupRequest에서 gender를 필수 필드로 검증하지만 (UserV1Dto.java 17줄), userFacade.signup에 전달하지 않아 데이터가 저장되지 않습니다. 사용자가 입력한 gender 정보가 유실됩니다.

다음과 같이 수정하세요:

-        UserInfo info = userFacade.signup(request.userId(), request.email(), request.birthDate());
+        UserInfo info = userFacade.signup(request.userId(), request.email(), request.gender(), request.birthDate());

UserFacade와 UserModel, UserService도 함께 수정이 필요합니다.

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

Comment on lines +22 to +23
@NotBlank(message = "gender는 필수입니다.")
String gender,
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

🧩 Analysis chain

gender 필드가 사용되지 않는지 확인하세요.

SignupRequest에 gender 필드가 있지만, UserFacade.signup 메서드(Line 16)에서 UserModel 생성 시 이 필드를 사용하지 않습니다. 필요 없는 필드라면 제거하거나, 사용해야 한다면 UserModel에 반영해야 합니다.

다음 스크립트로 gender 필드의 사용처를 확인하세요:


🏁 Script executed:

#!/bin/bash
# gender 필드가 실제로 사용되는지 확인
rg -n "\.gender\(\)" --type=java -C3

Length of output: 0


더 정확한 검증을 위해 추가 조사가 필요합니다. gender 필드의 사용처를 더 광범위하게 확인하겠습니다.


---

🏁 Script executed:

```shell
#!/bin/bash
# gender 필드 검색 - 더 광범위한 패턴으로 확인
echo "=== gender 필드 선언 및 사용 검색 ==="
rg -n "gender" --type=java -C2

echo ""
echo "=== UserFacade.signup 메서드 확인 ==="
rg -n "UserFacade|signup" --type=java -A10 -B2 | head -100

echo ""
echo "=== UserModel 클래스 확인 ==="
rg -n "class UserModel" --type=java -A20

Length of output: 16768


SignupRequest 클래스를 더 자세히 확인하여 최종 분석을 완료하겠습니다.


---

🏁 Script executed:

```shell
#!/bin/bash
# SignupRequest 전체 구조 확인
rg -n "record SignupRequest\|class SignupRequest" --type=java -A15

Length of output: 0



---

🏁 Script executed:

```shell
#!/bin/bash
# UserV1Dto.java 전체 파일 내용 확인
cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java

Length of output: 784


gender 필드를 SignupRequest에서 제거하거나 전체 처리 흐름에 반영하세요.

gender는 SignupRequest에서 필수 필드(@notblank)로 선언되어 있지만, UserV1Controller에서 UserFacade.signup() 호출 시 전달되지 않습니다. UserFacade와 UserModel에도 gender 매개변수와 필드가 없어 데이터가 버려집니다.

  • SignupRequest가 필요 없는 필드라면 gender 제거
  • 필요하다면 UserFacade.signup()과 UserModel에 gender 추가
🤖 Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java
around lines 22-23 the gender field is annotated @NotBlank but never propagated
to UserFacade/UserModel; either remove the unused field from the DTO or wire it
through the signup flow. If removing: delete the gender field and its
validation, update any clients/tests and mapping code that reference it. If
keeping: add a gender parameter to UserFacade.signup(...) signature and
corresponding DTO-to-model mapping, add a gender field to UserModel and any
persistence/repository objects and update controller calls to pass gender, and
ensure validation annotations remain consistent.

Comment on lines +69 to +74
UserV1Dto.SignupRequest request = new UserV1Dto.SignupRequest(
"user123",
"user123@example.com",
"male",
"1999-01-01"
);
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

🧩 Analysis chain

gender 필드가 사용되지 않는 문제를 확인하세요.

테스트에서 gender 필드를 제공하고 있지만 (line 72), UserFacade.signup 메서드는 userId, email, birthDate만 전달하고 gender는 사용하지 않습니다. SignupRequest에서 gender를 필수(@notblank)로 검증하지만 실제로 저장되지 않아 데이터 손실이 발생합니다.

다음 스크립트로 gender 필드의 사용 여부를 확인해보세요:


🏁 Script executed:

#!/bin/bash
# UserModel에 gender 관련 필드나 메서드가 있는지 확인
ast-grep --pattern 'class UserModel {
  $$$
}'

# UserFacade.signup에서 gender 파라미터 사용 여부 확인
ast-grep --pattern 'public UserInfo signup($$$) {
  $$$
}'

Length of output: 654


추가 검증이 필요합니다. 다음을 확인하겠습니다:

  • UserModel 클래스 구조와 gender 필드
  • SignupRequest DTO 정의
  • gender 필드의 사용 흐름

[/run_scripts]


🏁 Script executed:

#!/bin/bash
# UserModel 클래스 정의 및 필드 확인
ast-grep --pattern 'class UserModel {
  $$$
}' -l

# 파일 찾기 및 내용 확인
fd UserModel.java --type f -x cat {}

# SignupRequest DTO 구조 확인
fd SignupRequest -l

# gender 필드가 사용되는 모든 위치 확인
rg -n 'gender' --type java apps/commerce-api/src/main/java

Length of output: 2086


추가 검증이 필요합니다. 요청 처리 흐름을 확인하겠습니다:

[/run_scripts]


🏁 Script executed:

#!/bin/bash
# UserV1 API 컨트롤러에서 gender 처리 확인
fd UserV1Controller -l -x cat {}

# 테스트에서 응답 검증 내용 확인
rg -n -A 10 'SignupRequest' apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java

Length of output: 1332


gender 필드 데이터 손실 문제 확인됨 - 수정 필요

검증 결과:

  1. SignupRequest DTO: gender 필드를 @NotBlank(필수)로 검증 (UserV1Dto.java)
  2. UserModel: gender 필드 존재하지 않음 - userId, email, birthDate만 보유
  3. UserFacade.signup(): 3개 파라미터(userId, email, birthDate)만 수용
  4. 데이터 흐름: 테스트에서 gender를 API 요청으로 전송하지만 UserModel 저장 시 완전히 버려짐

해결 방안 (다음 중 선택):

  • 옵션 A: UserModel에 gender 필드 추가 + UserFacade.signup()에 gender 파라미터 추가 + 데이터 흐름 연결
  • 옵션 B: SignupRequest에서 gender 필드 제거 및 @notblank 검증 제거
🤖 Prompt for AI Agents
In
apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java
around lines 69 to 74, the test sends a gender value but the application
discards it because UserModel lacks a gender field and UserFacade.signup only
accepts userId, email, birthDate; fix by either (A) adding gender to the domain
and propagation path: add a gender field to UserModel, update
UserV1Dto.SignupRequest -> UserFacade.signup signature to include gender, update
service/repository mapping and persistence to store gender, and adjust tests
accordingly; or (B) remove gender from SignupRequest and related @NotBlank
validation, update API tests to stop sending gender, and ensure DTOs,
controllers, and validation reflect removal.

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