[volume-1] 회원가입/내 정보 조회/비밀번호 수정 기능 작성#7
Conversation
- Add ProductModel entity with name, description, price, stock fields - Add ProductRepository interface for domain abstraction - Add ProductService with CRUD business logic Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add ProductJpaRepository extending Spring Data JPA - Add ProductRepositoryImpl implementing domain repository - Support soft delete with findAllByDeletedAtIsNull Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add ProductInfo record for application DTO - Add ProductFacade for coordinating service calls Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add ProductV1Dto with Create/Update requests and responses
- Add ProductV1ApiSpec with OpenAPI annotations
- Add ProductV1Controller with CRUD endpoints:
- POST /api/v1/products (create)
- GET /api/v1/products/{id} (read single)
- GET /api/v1/products (read list with pagination)
- PUT /api/v1/products/{id} (update)
- DELETE /api/v1/products/{id} (soft delete)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Create .coderabbit.yaml
- Member 도메인 모델 및 레포지토리 생성 - 회원가입 API (POST /api/v1/members) 구현 - 비밀번호 검증: 8~16자, 영문대소문자/숫자/특수문자, 생년월일 포함 불가 - 로그인ID/이름/이메일 형식 검증 - 중복 로그인ID 검증 - BCrypt 비밀번호 암호화 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 내 정보 조회 API (GET /api/v1/members/me) 구현 - X-Loopers-LoginId, X-Loopers-LoginPw 헤더를 통한 인증 - 이름 마지막 글자 마스킹 처리 (*) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 비밀번호 수정 API (PUT /api/v1/members/password) 구현 - 기존 비밀번호 검증 후 새 비밀번호로 변경 - 현재 비밀번호와 동일한 비밀번호 사용 불가 - 비밀번호 규칙 검증 (8~16자, 생년월일 포함 불가) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the
✨ Finishing touches🧪 Generate unit tests (beta)
Important Action Needed: IP Allowlist UpdateIf your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:
Failure to add the new IP will result in interrupted reviews. 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. Comment |
There was a problem hiding this comment.
Pull request overview
회원 관리 기능(회원가입/내 정보 조회/비밀번호 변경)을 추가하고, 비밀번호 암호화를 위한 PasswordEncoder 설정을 도입한다. 추가로 상품 CRUD API/도메인도 함께 포함되며, PR 템플릿/리뷰봇 설정 및 기존 GitHub Actions 워크플로우 제거가 동반된다.
Changes:
- Member 도메인/애플리케이션/REST API 추가(회원가입, 내 정보 조회(이름 마스킹), 비밀번호 변경 + 유효성 검증/암호화)
- Product 도메인/애플리케이션/REST API 추가(CRUD + 페이징 조회 + soft delete 기반 삭제)
PasswordEncoder설정 추가, PR 템플릿/CodeRabbit 설정 추가 및 기존 PR Agent 워크플로우 제거
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/commerce-api/src/main/java/com/loopers/support/PasswordEncoderConfig.java | BCrypt 기반 PasswordEncoder 빈 등록 |
| apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java | 상품 요청/응답 DTO 및 Page 응답 매핑 추가 |
| apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java | 상품 CRUD + 목록 API 엔드포인트 추가 |
| apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java | 상품 API Swagger 스펙 인터페이스 추가 |
| apps/commerce-api/src/main/java/com/loopers/interfaces/api/member/MemberV1Dto.java | 회원 요청/응답 DTO 추가 |
| apps/commerce-api/src/main/java/com/loopers/interfaces/api/member/MemberV1Controller.java | 회원가입/내정보/비밀번호변경 API 엔드포인트 추가 |
| apps/commerce-api/src/main/java/com/loopers/interfaces/api/member/MemberV1ApiSpec.java | 회원 API Swagger 스펙 인터페이스 추가 |
| apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java | 상품 리포지토리 어댑터 구현(soft delete, 페이징 조회) |
| apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java | 상품 Spring Data JPA 리포지토리 추가 |
| apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberRepositoryImpl.java | 회원 리포지토리 어댑터 구현 추가 |
| apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberJpaRepository.java | 회원 Spring Data JPA 리포지토리 추가 |
| apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java | 상품 생성/조회/수정/삭제 도메인 서비스 추가 |
| apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java | 상품 도메인 리포지토리 인터페이스 추가 |
| apps/commerce-api/src/main/java/com/loopers/domain/product/ProductModel.java | 상품 엔티티 및 생성/수정 검증 로직 추가 |
| apps/commerce-api/src/main/java/com/loopers/domain/member/MemberService.java | 회원 등록/인증/비밀번호 변경 및 검증 로직 추가 |
| apps/commerce-api/src/main/java/com/loopers/domain/member/MemberRepository.java | 회원 도메인 리포지토리 인터페이스 추가 |
| apps/commerce-api/src/main/java/com/loopers/domain/member/MemberModel.java | 회원 엔티티 추가 |
| apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java | 상품 응답용 Info 레코드 추가 |
| apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java | 상품 유스케이스 파사드 추가 |
| apps/commerce-api/src/main/java/com/loopers/application/member/MemberInfo.java | 회원 Info 레코드 및 이름 마스킹 로직 추가 |
| apps/commerce-api/src/main/java/com/loopers/application/member/MemberFacade.java | 회원 유스케이스 파사드 추가 |
| apps/commerce-api/build.gradle.kts | spring-security-crypto 의존성 추가 |
| .github/workflows/main.yml | PR Agent GitHub Actions 워크플로우 제거 |
| .github/pull_request_template.md | PR 템플릿 개편(의사결정/설계/플로우 중심) |
| .coderabbit.yaml | CodeRabbit 리뷰 설정 신규 추가 |
| .codeguide/loopers-1-week.md | 가이드 문서 제거 |
Comments suppressed due to low confidence (1)
.github/pull_request_template.md:68
- PR 템플릿의 Mermaid 예시 코드 블록이 닫히지 않아(닫는 ``` 누락) 이후 내용이 전부 코드 블록으로 렌더링된다. Mermaid 블록 끝에 닫는 코드 펜스를 추가해야 한다.
### Main Flow
```mermaid
sequenceDiagram
autonumber
participant Client
participant API
participant Service
participant DB
Client->>API: request
API->>Service: command/query
Service->>DB: read/write
DB-->>Service: result
Service-->>API: response
API-->>Client: response
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| @Override | ||
| public Optional<ProductModel> find(Long id) { | ||
| return productJpaRepository.findById(id); |
There was a problem hiding this comment.
Product는 soft delete(BaseEntity.deletedAt)를 사용하고 있는데, find(id)가 단순 findById만 호출해서 삭제된 상품도 조회/수정/삭제 대상이 될 수 있다. 삭제된 상품을 NOT_FOUND로 처리하려면 repository 단에서 deletedAt IS NULL 조건을 포함한 조회(예: findByIdAndDeletedAtIsNull)를 사용하고, ProductService.getProduct()가 그 메서드를 사용하도록 정리하는 것이 안전하다.
| return productJpaRepository.findById(id); | |
| return productJpaRepository.findByIdAndDeletedAtIsNull(id); |
| @Override | ||
| public List<ProductModel> findAll() { | ||
| return productJpaRepository.findAll().stream() | ||
| .filter(product -> product.getDeletedAt() == null) | ||
| .collect(Collectors.toList()); | ||
| } |
There was a problem hiding this comment.
findAll()이 전체 데이터를 로딩한 뒤 스트림으로 deletedAt=null만 필터링하고 있어(soft delete 데이터가 많아지면) 불필요한 메모리/DB 부하가 발생한다. Pageable 버전처럼 JPA 쿼리로 deletedAt IS NULL 조건을 내려서 조회하도록 전용 메서드(예: findAllByDeletedAtIsNull())를 추가하는 편이 좋다.
| import org.springframework.data.domain.Page; | ||
| import org.springframework.data.domain.PageRequest; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
There was a problem hiding this comment.
ProductV1Controller에서 org.springframework.web.bind.annotation.* 와일드카드 임포트를 사용하고 있는데, 기존 컨트롤러들은 개별 임포트를 사용한다(예: ExampleV1Controller.java:7-10). 동일한 스타일로 정리해 불필요한 임포트 확장/충돌 가능성을 줄이는 것이 좋다.
| @PostMapping | ||
| @Override | ||
| public ApiResponse<MemberV1Dto.RegisterResponse> register(@Valid @RequestBody MemberV1Dto.RegisterRequest request) { | ||
| MemberInfo info = memberFacade.register( | ||
| request.loginId(), | ||
| request.password(), | ||
| request.name(), | ||
| request.birthDate(), | ||
| request.email() | ||
| ); | ||
| return ApiResponse.success(MemberV1Dto.RegisterResponse.from(info)); | ||
| } | ||
|
|
||
| @GetMapping("/me") | ||
| @Override | ||
| public ApiResponse<MemberV1Dto.MemberResponse> getMyInfo( | ||
| @RequestHeader("X-Loopers-LoginId") String loginId, | ||
| @RequestHeader("X-Loopers-LoginPw") String password | ||
| ) { | ||
| MemberInfo info = memberFacade.getMyInfo(loginId, password); | ||
| return ApiResponse.success(MemberV1Dto.MemberResponse.from(info)); | ||
| } | ||
|
|
||
| @PutMapping("/password") | ||
| @Override | ||
| public ApiResponse<Object> changePassword( | ||
| @RequestHeader("X-Loopers-LoginId") String loginId, | ||
| @RequestHeader("X-Loopers-LoginPw") String currentPassword, | ||
| @Valid @RequestBody MemberV1Dto.ChangePasswordRequest request | ||
| ) { | ||
| memberFacade.changePassword(loginId, currentPassword, request.newPassword()); | ||
| return ApiResponse.success(); | ||
| } |
There was a problem hiding this comment.
Member API(회원가입/내정보 조회/비밀번호 변경)와 관련된 E2E/통합 테스트가 추가되지 않았다. 기존에 ExampleV1ApiE2ETest/ExampleServiceIntegrationTest 패턴이 있으므로, 최소한 성공 케이스와 주요 실패 케이스(중복 loginId, 인증 실패, 비밀번호 규칙 위반 등)를 커버하는 테스트를 추가하는 것이 필요하다.
| @PostMapping | ||
| @Override | ||
| public ApiResponse<ProductV1Dto.ProductResponse> createProduct( | ||
| @Valid @RequestBody ProductV1Dto.CreateRequest request | ||
| ) { | ||
| ProductInfo info = productFacade.createProduct( | ||
| request.name(), | ||
| request.description(), | ||
| request.price(), | ||
| request.stock() | ||
| ); | ||
| ProductV1Dto.ProductResponse response = ProductV1Dto.ProductResponse.from(info); | ||
| return ApiResponse.success(response); | ||
| } | ||
|
|
||
| @GetMapping("/{productId}") | ||
| @Override | ||
| public ApiResponse<ProductV1Dto.ProductResponse> getProduct( | ||
| @PathVariable Long productId | ||
| ) { | ||
| ProductInfo info = productFacade.getProduct(productId); | ||
| ProductV1Dto.ProductResponse response = ProductV1Dto.ProductResponse.from(info); | ||
| return ApiResponse.success(response); | ||
| } | ||
|
|
||
| @GetMapping | ||
| @Override | ||
| public ApiResponse<ProductV1Dto.ProductListResponse> getProducts( | ||
| @RequestParam(defaultValue = "0") int page, | ||
| @RequestParam(defaultValue = "20") int size | ||
| ) { | ||
| PageRequest pageable = PageRequest.of(page, size); | ||
| Page<ProductInfo> productPage = productFacade.getProducts(pageable); | ||
| ProductV1Dto.ProductListResponse response = ProductV1Dto.ProductListResponse.from(productPage); | ||
| return ApiResponse.success(response); | ||
| } | ||
|
|
||
| @PutMapping("/{productId}") | ||
| @Override | ||
| public ApiResponse<ProductV1Dto.ProductResponse> updateProduct( | ||
| @PathVariable Long productId, | ||
| @Valid @RequestBody ProductV1Dto.UpdateRequest request | ||
| ) { | ||
| ProductInfo info = productFacade.updateProduct( | ||
| productId, | ||
| request.name(), | ||
| request.description(), | ||
| request.price(), | ||
| request.stock() | ||
| ); | ||
| ProductV1Dto.ProductResponse response = ProductV1Dto.ProductResponse.from(info); | ||
| return ApiResponse.success(response); | ||
| } | ||
|
|
||
| @DeleteMapping("/{productId}") | ||
| @Override | ||
| public ApiResponse<Object> deleteProduct( | ||
| @PathVariable Long productId | ||
| ) { | ||
| productFacade.deleteProduct(productId); | ||
| return ApiResponse.success(); | ||
| } |
There was a problem hiding this comment.
Product API(create/get/list/update/delete)와 soft delete 동작(삭제 후 목록/단건에서 제외)이 신규로 들어왔는데, 이를 보장하는 E2E/통합 테스트가 없다. 기존 Example 테스트 패턴처럼 주요 성공/실패 케이스와 soft delete 제외 조건을 검증하는 테스트를 추가하는 것이 필요하다.
| # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json | ||
|
|
||
| language: "ko-KR" | ||
| early_access: false | ||
|
|
There was a problem hiding this comment.
이 PR에서 .github/workflows/main.yml이 삭제된 뒤 현재 .github/workflows 디렉터리에 워크플로우가 남아있지 않다. 저장소가 GitHub Actions 기반 CI를 사용 중이었다면 빌드/테스트가 전혀 실행되지 않으므로, 의도된 변경인지 확인하고 최소한 build/test 워크플로우는 유지/대체하는 것이 안전하다.
| if (memberRepository.existsByLoginId(loginId)) { | ||
| throw new CoreException(ErrorType.CONFLICT, "이미 사용 중인 로그인 ID입니다."); | ||
| } | ||
|
|
||
| String encodedPassword = passwordEncoder.encode(password); | ||
| MemberModel member = new MemberModel(loginId, encodedPassword, name, birthDate, email); | ||
| return memberRepository.save(member); | ||
| } |
There was a problem hiding this comment.
register()에서 existsByLoginId 선확인 후 save 하는 방식은 동시 요청에서 레이스가 발생해 DB unique 제약 위반으로 500(INTERNAL_ERROR)로 떨어질 수 있다. save 시점의 unique 제약 위반(DataIntegrityViolationException 등)을 CONFLICT(CoreException)로 변환(또는 ApiControllerAdvice에서 해당 예외를 CONFLICT로 매핑)해 클라이언트가 일관된 응답을 받을 수 있게 처리하는 것이 필요하다.
| @RequiredArgsConstructor | ||
| @RestController | ||
| @RequestMapping("/api/v1/products") | ||
| public class ProductV1Controller implements ProductV1ApiSpec { | ||
|
|
||
| private final ProductFacade productFacade; | ||
|
|
There was a problem hiding this comment.
PR 제목/설명은 회원 기능(회원가입/내정보/비밀번호 변경) 중심인데, 이 PR에는 Product CRUD API/도메인도 함께 포함되어 있고(.interfaces/api/product, .domain/product 등), GitHub Actions 워크플로우 제거 및 CodeRabbit 설정 추가도 포함되어 범위가 크게 확장돼 있다. 리뷰/릴리즈 단위를 명확히 하기 위해 PR 설명을 변경 범위에 맞게 갱신하거나 PR을 분리하는 편이 좋다.
fix: UserQueryServiceTest 1자 이름 테스트 수정
refactor: DTO에 Bean Validation 적용 및 도메인 검증 책임 분리
📌 Summary
🧭 Context & Decision
문제 정의
선택지와 결정
🏗️ Design Overview
변경 범위
주요 컴포넌트 책임
MemberModel: 회원 엔티티 (loginId, password, name, birthDate, email)MemberService: 회원가입, 인증, 비밀번호 변경 비즈니스 로직 및 검증MemberFacade: 컨트롤러와 서비스 간 조율, 마스킹 처리MemberV1Controller: REST API 엔드포인트 제공🔁 Flow Diagram
Main Flow - 회원가입
Main Flow - 내 정보 조회
Main Flow - 비밀번호 수정