Skip to content

feat: 상점 정보 수정 기능 구현#27

Merged
JoonKyoLee merged 6 commits intomainfrom
feat/update-store
Nov 20, 2025
Merged

feat: 상점 정보 수정 기능 구현#27
JoonKyoLee merged 6 commits intomainfrom
feat/update-store

Conversation

@JoonKyoLee
Copy link
Contributor

@JoonKyoLee JoonKyoLee commented Nov 20, 2025

✨ 작업 내용

  • 상점 정보 수정 기능 구현

📝 적용 범위

  • /store

📌 참고 사항

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능
    • 상점 정보 수정 기능이 추가되었습니다. 사용자는 이제 상점 이름, 활성화 상태, 기본 재고 점검 임계값을 수정할 수 있습니다. 모든 입력값은 유효성 검사를 거쳐 안정성을 보장합니다.

✏️ Tip: You can customize this high-level summary in your review settings.

@JoonKyoLee JoonKyoLee self-assigned this Nov 20, 2025
@JoonKyoLee JoonKyoLee added the enhancement New feature or request label Nov 20, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 20, 2025

워크스루

상점 정보를 부분적으로 수정할 수 있는 기능을 구현합니다. REST PATCH 엔드포인트에서 상점명, 활성화 상태, 기본 재고 확인 임계값에 대한 선택적 업데이트를 수용하며, 입력값 검증 후 서비스 계층을 통해 변경사항을 데이터베이스에 반영합니다.

변경사항

조직화된 파일 / 변경 요약
API 응답 메시지
src/main/java/com/almang/inventory/global/api/SuccessMessage.java
데이터 전송 객체
src/main/java/com/almang/inventory/store/dto/request/UpdateStoreRequest.java, src/main/java/com/almang/inventory/store/dto/response/UpdateStoreResponse.java
도메인 계층
src/main/java/com/almang/inventory/store/domain/Store.java
비즈니스 로직 계층
src/main/java/com/almang/inventory/store/service/StoreService.java
REST 컨트롤러 계층
src/main/java/com/almang/inventory/store/controller/StoreController.java
테스트 스위트
src/test/java/com/almang/inventory/store/controller/StoreControllerTest.java, src/test/java/com/almang/inventory/store/service/StoreServiceTest.java

시퀀스 다이어그램

sequenceDiagram
    participant Client
    participant Controller as StoreController
    participant Service as StoreService
    participant Repo as Store/UserRepository
    participant Domain as Store Entity

    Client->>Controller: PATCH /api/v1/store<br/>(UpdateStoreRequest, userId)
    activate Controller
    
    Note over Controller: 요청 검증 & 로깅
    Controller->>Service: updateStore(request, userId)
    activate Service
    
    Service->>Repo: findUserById(userId)
    activate Repo
    Repo-->>Service: User found
    deactivate Repo
    
    Service->>Repo: findByUserId(user)
    activate Repo
    Repo-->>Service: Store entity
    deactivate Repo
    
    Note over Service: validateStoreName()<br/>validateThreshold()
    
    alt Validation Failed
        Service-->>Client: BaseException
    else Validation Passed
        Service->>Domain: updateName(name)
        Service->>Domain: updateActivation(isActivate)
        Service->>Domain: updateThreshold(threshold)
        
        Service->>Repo: save(Store)
        activate Repo
        Repo-->>Service: Updated Store
        deactivate Repo
        
        Note over Service: UpdateStoreResponse.from(store)
        Service-->>Controller: UpdateStoreResponse
    end
    
    deactivate Service
    
    Controller-->>Client: ResponseEntity<br/>ApiResponse<UpdateStoreResponse>
    deactivate Controller
Loading

리뷰 예상 소요 시간

🎯 3 (보통) | ⏱️ ~25분

주의 깊게 검토할 영역:

  • 필드 검증 로직 (StoreService.validateStoreName(), validateDefaultCountCheckThreshold()): 경계값 테스트 케이스가 충분한지 확인하세요. 특히 defaultCountCheckThreshold가 정확히 0.0과 1.0만 유효한지, 아니면 범위 내의 다른 소수값도 허용하는지 명확히 할 필요가 있습니다.

  • 트랜잭션 처리: StoreService.updateStore()@Transactional 어노테이션이 적용되어 있는지 확인하세요. 데이터 일관성을 위해 필수입니다.

  • 부분 업데이트 로직: UpdateStoreRequest의 필드들이 null 가능하다면, 서비스에서 null 체크 후에만 해당 필드를 업데이트하는지 검증하세요. 현재 구현이 모든 필드를 필수로 처리하는지 선택적으로 처리하는지 확인이 필요합니다.

  • 테스트 커버리지: 성공 케이스뿐만 아니라 경계값(상점명 정확히 20자, 임계값 정확히 0.0/1.0)에 대한 테스트가 있는지 검토하세요.

  • 사용자 권한 검증: 인증된 사용자가 본인의 상점만 수정할 수 있도록 권한 검증이 제대로 되어 있는지 확인하세요.

관련된 PR

축시

🏪✨ 상점 수정 기능, 이제 부분 수정까지 가능하네!
어떤 필드든 선택해서 쓱싹— 업데이트,
검증도 탄탄하고 테스트도 많고,
도메인 메서드는 깔끔하고,
계층도 분리돼 있으니 앞으로 확장도 쉬울 것 같군요! 👏

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 변경 내용의 핵심을 명확하게 설명합니다. '상점 정보 수정 기능 구현'은 간결하고 구체적이며 전체 변경 사항을 잘 요약합니다.
Description check ✅ Passed PR 설명이 제공된 템플릿 구조를 따르고 있으며, 작업 내용, 적용 범위, 참고 사항이 모두 포함되어 있습니다.
Linked Issues check ✅ Passed PR의 모든 코드 변경 사항이 Issue #25의 요구사항을 충족합니다. 부분 수정 가능 기능이 UpdateStoreRequest와 optional 필드 처리를 통해 구현되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경 사항이 '상점 정보 수정 기능' 구현이라는 범위 내에 있으며, 불필요한 외부 변경 사항이 없습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/update-store

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: 1

🧹 Nitpick comments (6)
src/main/java/com/almang/inventory/store/domain/Store.java (1)

47-57: 도메인 엔티티에 업데이트 메서드를 둔 선택이 좋습니다

필드별 업데이트 메서드를 열어두어 서비스에서 부분 수정 로직을 깔끔하게 표현할 수 있는 점이 좋습니다.
다만 도메인 규칙(예: 이름 공백/길이 제한, 임계치 범위 등)을 한 곳에서 보장하려면, 현재 DTO·서비스에 흩어져 있을 검증 일부를 점진적으로 이 메서드들 안으로 옮겨오는 것도 고려해 볼 만합니다. 이미 defaultCountCheckThreshold는 Bean Validation 어노테이션으로 0~1 범위를 보장하고 있으니, 이름 정규화(trim)나 추가 길이 체크도 여기서 함께 다루면 Store 상태 변경 진입점이 더 명확해집니다. 이런 도메인 불변식 캡슐화 패턴은 DDD 자료와 Bean Validation 공식 문서를 같이 보시면 설계 방향에 도움이 됩니다.

src/main/java/com/almang/inventory/store/dto/response/UpdateStoreResponse.java (1)

1-20: UpdateStoreResponse 레코드 설계가 단순하고 명확합니다

도메인 엔티티를 직접 노출하지 않고 응답 전용 레코드로 감싸고, 정적 팩터리 from(Store)로 매핑 책임을 분리한 구조가 깔끔합니다.
장기적으로 Store 필드 구조가 바뀌거나 응답 요구사항이 늘어날 수 있으니, 전용 매퍼(예: MapStruct 기반 매퍼나 수동 매퍼 클래스)를 하나 두고 이 레코드를 그 매퍼를 통해 생성하도록 추상화를 한 단계 더 두면 컨트롤러·서비스가 도메인 변경에 덜 민감해집니다. Mapper 패턴 관련 예시는 MapStruct 공식 문서를 참고해 보셔도 좋겠습니다.

src/test/java/com/almang/inventory/store/service/StoreServiceTest.java (2)

31-155: 부분 수정·동시 수정 시나리오까지 잘 커버한 통합 테스트입니다

name/임계치/활성화 여부를 각각·조합으로 수정하는 케이스를 분리해 둬서, “수정하고 싶은 부분만 수정”이라는 요구사항을 테스트 이름만 봐도 이해할 수 있는 점이 좋습니다.
다만 현재는 DB에서 다시 조회한 Store만 상세히 검증하고 UpdateStoreResponseisNotNull()만 확인하고 있는데, 응답 DTO 필드(storeId, name, isActivate, defaultCountCheckThreshold)도 함께 검증해 두면 서비스 로직뿐 아니라 API 계약과 매핑까지 한 번에 보호할 수 있습니다. 예를 들어 assertThat(response.name()).isEqualTo("수정된 상점 이름")처럼 응답 값과 엔티티 값을 나란히 비교해 두면, 매핑 코드가 변경되었을 때 회귀를 더 빨리 잡을 수 있습니다.


157-226: 예외 케이스 테스트도 충실하지만, 검증 단위를 조금 더 정교하게 다듬을 수 있습니다

USER_NOT_FOUND, 상점 이름 길이 초과, 임계치 범위 초과(<0, >1) 같은 주요 실패 경로를 개별 테스트로 잘 분리해 두신 점 좋습니다.
다만 .hasMessageContaining(ErrorCode.XXX.getMessage())에 의존하면 나중에 에러 메시지 문구만 바뀌어도 테스트가 깨질 수 있어 유지보수성이 떨어질 수 있습니다. BaseExceptionErrorCode를 필드로 노출한다면, AssertJ의 extracting("errorCode").isEqualTo(ErrorCode.XXX)처럼 코드 자체를 검증하는 방식으로 바꾸는 것을 추천드립니다. 또 임계치 범위 테스트 두 개(-0.1, 1.1)는 JUnit @ParameterizedTest로 하나의 테스트로 묶으면 중복을 줄이면서도 의도는 그대로 드러낼 수 있습니다.

src/main/java/com/almang/inventory/store/dto/request/UpdateStoreRequest.java (1)

9-9: 빈 문자열 검증 추가를 권장합니다.

name 필드에 @Size(max = 20)만 있어서 빈 문자열("")이나 공백 문자열(" ")이 허용됩니다. 상점명을 수정할 때 의미 없는 값이 저장되지 않도록 @NotBlank 어노테이션을 추가하는 것이 좋습니다.

다음과 같이 수정하세요:

-        @Size(max = 20) String name,
+        @NotBlank @Size(max = 20) String name,

참고: Bean Validation 스펙에서 @NotBlank는 null이 아니면서 공백이 아닌 문자를 최소 하나 포함해야 합니다.

src/main/java/com/almang/inventory/store/service/StoreService.java (1)

30-42: 최소 하나의 필드 제공 여부를 검증하는 것이 좋습니다.

현재는 모든 필드가 null인 요청도 허용되어 실제로 아무것도 변경하지 않고 성공 응답을 반환합니다. 사용자 경험과 API 명확성을 위해 최소 하나의 필드가 제공되었는지 검증하는 것을 권장합니다.

메서드 시작 부분에 검증 로직을 추가하세요:

     public UpdateStoreResponse updateStore(UpdateStoreRequest request, Long userId) {
         User user = findUserById(userId);
         Store store = user.getStore();

+        if (request.name() == null 
+                && request.defaultCountCheckThreshold() == null 
+                && request.isActivate() == null) {
+            throw new BaseException(ErrorCode.NO_FIELD_TO_UPDATE);
+        }
+
         log.info("[StoreService] 상점 정보 수정 요청 - userId: {}, storeId: {}", userId, store.getId());

참고: 이런 검증은 비즈니스 규칙이므로 서비스 레이어에서 처리하는 것이 적절합니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0867a27 and 3241e75.

📒 Files selected for processing (8)
  • src/main/java/com/almang/inventory/global/api/SuccessMessage.java (1 hunks)
  • src/main/java/com/almang/inventory/store/controller/StoreController.java (1 hunks)
  • src/main/java/com/almang/inventory/store/domain/Store.java (1 hunks)
  • src/main/java/com/almang/inventory/store/dto/request/UpdateStoreRequest.java (1 hunks)
  • src/main/java/com/almang/inventory/store/dto/response/UpdateStoreResponse.java (1 hunks)
  • src/main/java/com/almang/inventory/store/service/StoreService.java (1 hunks)
  • src/test/java/com/almang/inventory/store/controller/StoreControllerTest.java (1 hunks)
  • src/test/java/com/almang/inventory/store/service/StoreServiceTest.java (1 hunks)
🔇 Additional comments (6)
src/main/java/com/almang/inventory/global/api/SuccessMessage.java (1)

10-22: 상점 수정 성공 메시지 상수 추가 👍

STORE 섹션의 기존 패턴(CREATE_STORE_SUCCESS)과 이름·문구가 잘 맞아서 응답 메시지 일관성이 유지됩니다.
향후 국제화(i18n)가 필요해지면, 이 enum 값을 메시지 키처럼 사용하고 실제 문자열은 MessageSource/리소스 번들로 분리하는 구조도 한 번 고려해 보시면 좋겠습니다. 스프링 메시지 국제화 가이드를 같이 보면 설계 방향을 잡는 데 도움이 됩니다.

src/main/java/com/almang/inventory/store/controller/StoreController.java (1)

30-42: PATCH 상점 수정 엔드포인트 구성이 명확합니다

요청 DTO에 @Valid를 적용하고 인증 정보에서 userId만 꺼내 서비스에 위임한 구조라, 책임 분리가 잘 되어 있고 유지보수하기 좋아 보입니다. 응답도 공통 ApiResponse + SuccessMessage.UPDATE_STORE_SUCCESS 패턴을 그대로 따라가서 클라이언트 입장에서도 예측 가능하겠습니다.
실서비스 관점에서 한 가지만 더 점검해 볼 부분은 userPrincipal이 null일 수 있는지 보안 설정입니다. 익명 접근이 완전히 차단되어 있다면 지금도 충분하지만, 혹시 모를 설정 변경에 대비해 메서드 초반에 방어적 체크나 Spring Security의 @PreAuthorize를 추가하는 것도 선택지입니다. 또한 OpenAPI/Swagger 설정 쪽에서 이 엔드포인트가 인증이 필요한 API임을 명시(security 섹션)해 두면 프론트엔드/외부 소비자들이 스펙을 읽기 더 편해집니다.

src/main/java/com/almang/inventory/store/dto/request/UpdateStoreRequest.java (1)

8-13: 부분 수정 기능 구현이 깔끔합니다! 👍

nullable 필드를 활용한 부분 업데이트 구조가 요구사항을 잘 반영하고 있습니다.

src/main/java/com/almang/inventory/store/service/StoreService.java (1)

23-46: 트랜잭션 처리와 로깅이 잘 구현되었습니다.

JPA의 더티 체킹(dirty checking)을 활용한 엔티티 업데이트 방식이 적절하며, 로깅도 요청/성공 시점에 명확하게 남기고 있습니다.

src/test/java/com/almang/inventory/store/controller/StoreControllerTest.java (2)

51-125: 테스트 커버리지가 훌륭합니다! 🎯

성공 케이스, 사용자 미존재 에러, 그리고 입력값 검증 실패 시나리오를 모두 커버하고 있습니다. 특히 Line 111에서 21자 이름으로 경계값 테스트를 수행한 점이 좋습니다.


43-49: 인증 헬퍼 메서드 재사용이 깔끔합니다.

테스트 인증 토큰 생성 로직을 별도 메서드로 분리해 중복을 제거한 점이 좋습니다.

Comment on lines +53 to +64
private void validateStoreName(String storeName) {
if (storeName.length() > 20) {
throw new BaseException(ErrorCode.STORE_NAME_IS_LONG);
}
}

private void validateDefaultCountCheckThreshold(BigDecimal defaultCountCheckThreshold) {
if (defaultCountCheckThreshold.compareTo(BigDecimal.ZERO) < 0
|| defaultCountCheckThreshold.compareTo(BigDecimal.ONE) > 0) {
throw new BaseException(ErrorCode.DEFAULT_COUNT_CHECK_THRESHOLD_NOT_IN_RANGE);
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

중복 검증 로직을 제거하세요.

Lines 54-56의 상점명 길이 검증과 Lines 60-62의 threshold 범위 검증은 UpdateStoreRequest의 Bean Validation 어노테이션(@SiZe, @DecimalMin, @DecimalMax)과 중복됩니다. Controller에서 @Valid 어노테이션으로 DTO 검증이 이미 수행되므로, 서비스 레이어의 이 메서드들은 불필요합니다.

중복 검증 메서드를 제거하고 간결하게 리팩토링하세요:

     if (request.name() != null) {
-        validateStoreName(request.name());
         store.updateName(request.name());
     }

     if (request.defaultCountCheckThreshold() != null) {
-        validateDefaultCountCheckThreshold(request.defaultCountCheckThreshold());
         store.updateThreshold(request.defaultCountCheckThreshold());
     }

-    private void validateStoreName(String storeName) {
-        if (storeName.length() > 20) {
-            throw new BaseException(ErrorCode.STORE_NAME_IS_LONG);
-        }
-    }
-
-    private void validateDefaultCountCheckThreshold(BigDecimal defaultCountCheckThreshold) {
-        if (defaultCountCheckThreshold.compareTo(BigDecimal.ZERO) < 0
-                || defaultCountCheckThreshold.compareTo(BigDecimal.ONE) > 0) {
-            throw new BaseException(ErrorCode.DEFAULT_COUNT_CHECK_THRESHOLD_NOT_IN_RANGE);
-        }
-    }

참고: Spring의 @Valid 어노테이션은 컨트롤러 레벨에서 자동으로 검증하며, 실패 시 MethodArgumentNotValidException을 발생시킵니다. 서비스 레이어에서 다시 검증하는 것은 불필요한 중복입니다.

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

🤖 Prompt for AI Agents
In src/main/java/com/almang/inventory/store/service/StoreService.java around
lines 53 to 64, remove the two private validation methods validateStoreName and
validateDefaultCountCheckThreshold because those checks are already enforced by
Bean Validation annotations on UpdateStoreRequest and handled via @Valid in the
controller; delete their declarations and any internal calls to them so the
service no longer duplicates DTO validation, keeping the service focused on
business logic only.

@JoonKyoLee JoonKyoLee merged commit d71f67a into main Nov 20, 2025
1 check passed
@JoonKyoLee JoonKyoLee deleted the feat/update-store branch November 20, 2025 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 상점 수정 기능 구현

1 participant