Skip to content

[Feature] (관리자) 상품 옵션 재고 상태 변경 API#565

Merged
kmindev merged 12 commits intodevfrom
feature/add_edit_product_stock_feature
Feb 5, 2026
Merged

[Feature] (관리자) 상품 옵션 재고 상태 변경 API#565
kmindev merged 12 commits intodevfrom
feature/add_edit_product_stock_feature

Conversation

@MSBANG
Copy link
Contributor

@MSBANG MSBANG commented Jan 18, 2026

History

🚀 Major Changes & Explanations

  • AdminProductService, AdminProductController 추가
  • 상품 재고 증가, 감소, 품절 처리 API 구현
  • 동시성 제어를 위해 DB 비관락 적용(select for update)

📷 Test Image

image

💡 ETC

Summary by CodeRabbit

  • 새로운 기능
    • 관리자용 상품 재고 관리 기능이 추가되었습니다. 상품 옵션별로 재고를 증가·감소하거나 품절 처리할 수 있으며, 음수 재고 방지 및 부족 시 예외 처리를 포함한 입력 검증이 적용됩니다.
  • 테스트
    • 단위·통합 테스트와 동시성 테스트가 추가되어 다양한 재고 조작 시나리오(증가/감소/품절/동시 작업)를 검증합니다.

@coderabbitai
Copy link

coderabbitai bot commented Jan 18, 2026

Walkthrough

관리자용 상품 옵션 재고를 수정하는 REST API와 서비스·도메인 로직, 예외 코드 및 단위·통합·동시성 테스트가 추가됩니다.

Changes

Cohort / File(s) Summary
컨트롤러 및 API
src/main/java/com/bbangle/bbangle/board/admin/controller/AdminProductController.java, src/main/java/com/bbangle/bbangle/board/admin/controller/swagger/AdminProductApi.java
PATCH /options/{optionId}/stock 엔드포인트 추가(요청 검증→서비스 위임→성공응답). Swagger 문서화.
요청 DTO
src/main/java/com/bbangle/bbangle/board/admin/controller/dto/AdminEditStockRequest.java
레코드 추가: editStockFlag, amount. compact constructor로 amount < 0 검증 및 예외 발생.
서비스 레이어
src/main/java/com/bbangle/bbangle/board/admin/service/AdminProductService.java
상품을 PESSIMISTIC_WRITE 락으로 조회(findWithLockById)하고 editStock 호출. 존재하지 않으면 예외 발생.
도메인 변경
src/main/java/com/bbangle/bbangle/board/domain/Product.java, src/main/java/com/bbangle/bbangle/board/domain/EditStockFlag.java
Product에 editStock(int, EditStockFlag) 추가(증가/감소/품절 로직). 재고 변경 유형을 정의하는 enum 추가.
리포지토리
src/main/java/com/bbangle/bbangle/board/repository/ProductRepository.java
Optional<Product> findWithLockById(Long id) 추가, @Lock(LockModeType.PESSIMISTIC_WRITE) 적용.
에러 코드
src/main/java/com/bbangle/bbangle/exception/BbangleErrorCode.java
INVALID_STOCK_AMOUNT, NOT_FOUND_OPTION, INVALID_DECREASE_STOCK_AMOUNT 3개 코드 추가.
테스트: 통합 / 동시성 / 단위
src/test/.../AdminProductServiceIntegrationTest.java, .../AdminProductServiceConcurrencyTest.java, .../ProductTest.java
AdminProductService와 Product.editStock 행위에 대한 다양한 시나리오(증가·감소·품절·부족·미존재) 및 동시성(멀티스레드) 검증 테스트 추가.
테스트 픽스처
src/test/java/com/bbangle/bbangle/fixture/board/domain/ProductFixture.java
createWithStock(Board, String, int) 헬퍼 추가.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Controller as AdminProductController
    participant Service as AdminProductService
    participant Repo as ProductRepository
    participant Domain as Product
    participant Response as ResponseService
    participant Exception as ExceptionHandler

    Client->>Controller: PATCH /options/{optionId}/stock (AdminEditStockRequest)
    Controller->>Service: editProductStock(optionId, amount, flag)
    Service->>Repo: findWithLockById(optionId)
    alt not found
        Repo-->>Service: Optional.empty()
        Service->>ExceptionHandler: throw BbangleException(NOT_FOUND_OPTION)
        ExceptionHandler-->>Client: ErrorResponse
    else found
        Repo-->>Service: Product
        Service->>Domain: editStock(amount, flag)
        alt invalid decrease
            Domain->>ExceptionHandler: throw BbangleException(INVALID_DECREASE_STOCK_AMOUNT)
            ExceptionHandler-->>Client: ErrorResponse
        else success
            Domain-->>Service: void
            Service-->>Controller: void
            Controller->>Response: getSuccessResult()
            Response-->>Client: CommonResult(success)
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

feat, test

Suggested reviewers

  • CUCU7103
  • kimbro97

Poem

🐰 재고를 톡! 짜잔 늘리고 줄이고,
버튼 하나에 흩어진 당근들,
테스트는 바삭바삭 검증했네,
관리자님 웃음 한 움큼,
훗— 당근 파티 열자! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 주요 변경사항을 명확하게 요약하고 있으며, PR의 핵심 기능인 관리자용 상품 옵션 재고 상태 변경 API 구현을 직접적으로 설명하고 있습니다.
Linked Issues check ✅ Passed PR의 변경사항이 issue #542의 요구사항을 충분히 충족하고 있습니다. 상품 재고 증가, 감소, 품절 처리 기능이 구현되었으며 API 엔드포인트도 제공되고 있습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 상품 재고 관리 기능과 관련된 범위 내에 있습니다. 컨트롤러, 서비스, 도메인 모델, 예외 처리, 테스트 코드 등이 모두 동일한 기능 목표에 부합합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feature/add_edit_product_stock_feature

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.

@MSBANG MSBANG force-pushed the feature/add_edit_product_stock_feature branch from 459b999 to 784837c Compare January 23, 2026 13:11
@MSBANG MSBANG marked this pull request as ready for review January 23, 2026 13:14
@MSBANG MSBANG requested review from CUCU7103, exjuu and kmindev January 23, 2026 13:14
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: 9

🤖 Fix all issues with AI agents
In
`@src/main/java/com/bbangle/bbangle/board/admin/controller/AdminBoardController.java`:
- Around line 54-61: The controller currently extracts a productId in
AdminBoardController.deleteProducts and constructs
RemoveProductsCommand(productId), but the service layer uses
softDeleteByBoardId—causing mismatched semantics and risk of deleting the wrong
board's products; fix by making the identifier semantics consistent: either
rename the path and parameter to boardId (change
`@DeleteMapping`("{boardId}/options"), `@PathVariable` Long boardId, and new
RemoveProductsCommand(boardId, ...)) so the command and
adminBoardService.deleteProducts/softDeleteByBoardId align, or keep productId
semantics and update the controller to call a product-specific service method
(e.g., adminBoardService.deleteProductOptionsByProductId) and change
RemoveProductsCommand to accept productId; update DTO/variable names
(request.optionIds(), request.removeAll()) accordingly wherever
RemoveProductsCommand and adminBoardService methods are defined so the
identifier meaning is unambiguous.

In
`@src/main/java/com/bbangle/bbangle/board/admin/controller/dto/AdminEditStockRequest.java`:
- Around line 10-18: Add validation in the AdminEditStockRequest compact
constructor to guard against null editStockFlag and to enforce flag-specific
amount rules: check editStockFlag != null and throw a BbangleException with an
appropriate error code if null; then validate amount against EditStockFlag
(e.g., for INCREASE and DECREASE require amount > 0, for SOLDOUT allow amount ==
0 but reject amount > 0, etc.) and throw BbangleException (use or add relevant
error constants, not just INVALID_STOCK_AMOUNT) when rules are violated so
downstream domain logic cannot hit NPEs or accept meaningless requests.

In
`@src/main/java/com/bbangle/bbangle/board/admin/controller/dto/AdminRemoveProductRequest.java`:
- Around line 17-19: The null-check for optionIds is missing in the
AdminRemoveProductRequest validation, so calling optionIds.isEmpty() can NPE
when optionIds is absent; update the validation in AdminRemoveProductRequest
(the constructor/initializer that currently checks if (!removeAll &&
optionIds.isEmpty())) to explicitly handle null (e.g., treat null as empty by
checking optionIds == null || optionIds.isEmpty() or use a utility like
CollectionUtils.isEmpty(optionIds)) and still throw BbangleException with
INVALID_REMOVE_PRODUCTS_REQUEST when removeAll is false and optionIds is
null/empty.

In
`@src/main/java/com/bbangle/bbangle/board/admin/controller/swagger/AdminBoardApi.java`:
- Around line 36-39: The Operation annotation in AdminBoardApi has a typo in the
description string ("선책한" should be "선택한"); update the description attribute of
the `@Operation` on the admin product option delete API (in the AdminBoardApi
class) to use "선택한" instead of "선책한" so the description reads "관리자가 선택한 상품의 옵션을
삭제합니다."

In
`@src/main/java/com/bbangle/bbangle/board/admin/service/dto/RemoveProductsCommand.java`:
- Around line 6-15: The Swagger descriptions in RemoveProductsCommand are
inconsistent with the field names (boardId, productIds) and mention both "상품"
and "옵션"; update them so each field's `@Schema` accurately reflects its real
domain meaning: confirm whether boardId is a "게시글 ID" or "상품 ID" and change the
annotation text (or rename the field) accordingly; similarly, if productIds
refers to option IDs rename it to optionIds or change the description to "삭제 대상
상품 ID List" / "삭제 대상 옵션 ID List" to match removeAll semantics; ensure
removeAll's description clearly states when productIds/optionIds is required and
keep the class name RemoveProductsCommand unchanged.

In `@src/main/java/com/bbangle/bbangle/board/domain/Product.java`:
- Around line 184-198: The editStock method in Product must validate negative
amounts and clear soldout on increases: add an initial guard in editStock(int
amount, EditStockFlag editStockFlag) to throw a BbangleException for amount <= 0
(or specifically amount < 0 if zero is allowed) using an appropriate
BbangleErrorCode (e.g., INVALID_STOCK_AMOUNT or reuse
INVALID_DECREASE_STOCK_AMOUNT if that's the intended code), then in the INCREASE
branch set this.soldout = false after increasing stock (this.stock += amount) to
reopen soldout products; keep the DECREASE branch checks (ensure this.stock >=
amount) but ensure amount is positive by the initial guard; leave SOLDOUT
behavior setting stock = 0 and soldout = true. Ensure you reference the
editStock method, the EditStockFlag enum values (INCREASE, DECREASE, SOLDOUT),
and throw BbangleException with the chosen BbangleErrorCode for invalid amounts.
- Around line 106-108: Product currently redeclares the isDeleted field that is
already mapped in SoftDeleteBaseEntity, causing duplicate JPA column mapping;
remove the isDeleted declaration from the Product class so it inherits the field
and mapping from SoftDeleteBaseEntity (if you need access in Product, use the
inherited getter/setter or override accessor methods without re-declaring the
field or `@Column`).

In `@src/main/java/com/bbangle/bbangle/board/repository/ProductRepository.java`:
- Around line 23-29: The JPQL in method softDeleteByBoardId has a mismatched
parameter name (:boardIds) that doesn't match the `@Param`("boardId") binding;
update the `@Query` in softDeleteByBoardId to use :boardId so the JPQL parameter
name matches the `@Param` and the update executes correctly.

In `@src/main/java/com/bbangle/bbangle/exception/BbangleErrorCode.java`:
- Around line 76-79: The enum BbangleErrorCode contains user-facing Korean
messages with spacing/typos that need polishing; update the message strings for
INVALID_REMOVE_PRODUCTS_REQUEST, INVALID_STOCK_AMOUNT, NOT_FOUND_OPTION, and
INVALID_DECREASE_STOCK_AMOUNT in BbangleErrorCode to correct grammar and spacing
(e.g., make them natural Korean: "옵션 전체 삭제가 아닐 경우 옵션 ID가 하나 이상 있어야 합니다.", "재고
수량은 0 이상이어야 합니다.", "존재하지 않는 상품 옵션입니다.", "감소하려는 수량이 현재 재고보다 큽니다.") so the
user-facing text is clear and consistent while keeping the same enum names and
error codes.
🧹 Nitpick comments (2)
src/test/java/com/bbangle/bbangle/fixture/board/domain/ProductFixture.java (1)

18-24: stock 값과 soldout 상태의 일관성을 맞추는 게 좋습니다.

stock=0인 케이스에서도 soldout이 false로 고정됩니다. 테스트 신뢰성을 위해 stock 기반으로 설정을 권장합니다.

🧩 제안 수정
 return Product.builder()
     .title(title)
     .board(board)
     .stock(stock)
-    .soldout(false)
+    .soldout(stock == 0)
     .build();
src/test/java/com/bbangle/bbangle/board/admin/service/AdminProductServiceIntegrationTest.java (1)

55-61: DB 반영 검증을 위해 영속성 컨텍스트를 비워주세요.

flush() 후 동일 컨텍스트에서 findById()를 호출하면 캐시된 엔티티가 그대로 반환될 수 있어 “DB 반영” 검증이 약해집니다. EntityManager.clear()(또는 flushAndClear() 헬퍼)로 컨텍스트를 비운 뒤 재조회하는 방식이 더 안전합니다.

♻️ 예시 변경
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
 ...
     `@Autowired`
     private StoreRepository storeRepository;
+
+    `@PersistenceContext`
+    private EntityManager entityManager;
 ...
             adminProductService.editProductStock(product.getId(), 5, EditStockFlag.INCREASE);
             productRepository.flush();
+            entityManager.clear();
 
             // then
             Product updatedProduct = productRepository.findById(product.getId()).orElseThrow();

Copy link
Collaborator

@CUCU7103 CUCU7103 left a comment

Choose a reason for hiding this comment

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

@MSBANG 작업하느라 고생 많으셨습니다 민수님!
제가 보기에는 지금 코드래빗에서 잡은 오류들 위주로 처리하면 크게 문제되어질 부분은 없다고 생각되어집니다!

@MSBANG MSBANG changed the base branch from dev to feature/add_remove_product_option_feature January 28, 2026 12:14
@kmindev kmindev force-pushed the feature/add_remove_product_option_feature branch from 29077e7 to d2dc0f1 Compare February 4, 2026 12:58
Base automatically changed from feature/add_remove_product_option_feature to dev February 4, 2026 13:00
MSBANG added 10 commits February 4, 2026 22:06
1. 기본값 False 로 추가
2. builder 추가
1. EditStockFlag Enum 추가
2. 재고 증가, 감소, 품절 처리 기능 추가
3. 감소하려는 수보다 재고가 적을 경우 예외처리
4. 재고가 0이 될경우 자동 품절 처리
5. 품절 요청 시 재고 0으로 변경
1. command 없이 재고 수정 기능 추가
1. amount 0 이하 Validation 추가
1. Swagger 문서 추가
2. Controller Service 연동
1. 재고 증가할 때 soldout 필드 확인 로직 추가
@kmindev kmindev force-pushed the feature/add_edit_product_stock_feature branch from 1adbd1e to 68caf6f Compare February 4, 2026 13:10
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

🤖 Fix all issues with AI agents
In
`@src/main/java/com/bbangle/bbangle/board/admin/service/AdminProductService.java`:
- Around line 18-24: Rename the mismatched parameter in
AdminProductService.editProductStock to match the API/domain term "option"
(e.g., change the parameter name productId to optionId) and update any local
variable names and usages inside the method (including the call to
productRepository.findById(...) if that lookup should target an option
repository or keep as-is only renaming the identifier). Ensure the method
signature, any callers, and related exception message
(BbangleException(BbangleErrorCode.NOT_FOUND_OPTION)) remain consistent with the
"option" terminology (or alternatively harmonize all names to "product" across
controller, service, and error code if you choose that approach).
🧹 Nitpick comments (3)
src/main/java/com/bbangle/bbangle/board/domain/Product.java (1)

186-204: 재고 증가 시 품절 해제 로직이 적절하게 구현되었습니다.

이전 리뷰에서 지적된 soldout 해제 로직이 잘 반영되었습니다.

선택적 개선 제안: if-else 체인 대신 switch 표현식을 사용하면 가독성이 향상됩니다.

,

♻️ switch 표현식으로 리팩토링 제안
 public void editStock(int amount, EditStockFlag editStockFlag) {
-    if (editStockFlag == EditStockFlag.INCREASE) {
-        this.stock = this.stock + amount;
-        if (this.soldout) {
-            this.soldout = false;
-        }
-    } else if (editStockFlag == EditStockFlag.DECREASE) {
-        if (this.stock < amount) {
-            throw new BbangleException(BbangleErrorCode.INVALID_DECREASE_STOCK_AMOUNT);
-        }
-        this.stock = this.stock - amount;
-        if (this.stock == 0) {
-            this.soldout = true;
-        }
-    } else if (editStockFlag == EditStockFlag.SOLDOUT) {
-        this.stock = 0;
-        this.soldout = true;
+    switch (editStockFlag) {
+        case INCREASE -> {
+            this.stock += amount;
+            this.soldout = false;
+        }
+        case DECREASE -> {
+            if (this.stock < amount) {
+                throw new BbangleException(BbangleErrorCode.INVALID_DECREASE_STOCK_AMOUNT);
+            }
+            this.stock -= amount;
+            this.soldout = (this.stock == 0);
+        }
+        case SOLDOUT -> {
+            this.stock = 0;
+            this.soldout = true;
+        }
     }
 }
src/test/java/com/bbangle/bbangle/board/admin/service/AdminProductServiceIntegrationTest.java (2)

47-63: INCREASE 테스트에서 품절 해제 검증 누락

현재 INCREASE 테스트는 품절 상태가 아닌 상품만 테스트합니다. Product.editStock에서 품절 상태 해제 로직이 추가되었으므로, 품절 상태인 상품에 재고를 증가시키면 품절이 해제되는 시나리오도 테스트해야 합니다.

🧪 품절 해제 테스트 추가 제안
`@Test`
`@DisplayName`("INCREASE: 품절 상태인 상품에 재고를 증가시키면 품절이 해제된다")
void increase_soldoutProduct_clearsSoldout() {
    // given
    Store store = storeRepository.save(StoreFixture.defaultStore());
    Board board = boardRepository.save(BoardFixture.crawlingActiveBoardWithStore(store, "테스트게시글"));
    Product product = Product.builder()
        .title("품절상품")
        .board(board)
        .stock(0)
        .soldout(true)
        .build();
    productRepository.save(product);

    // when
    adminProductService.editProductStock(product.getId(), 5, EditStockFlag.INCREASE);
    productRepository.flush();

    // then
    Product updatedProduct = productRepository.findById(product.getId()).orElseThrow();
    assertThat(updatedProduct.getStock()).isEqualTo(5);
    assertThat(updatedProduct.isSoldout()).isFalse();
}

136-146: 예외 테스트에서 에러 코드 검증 추가 권장

BbangleException 발생 여부만 확인하고 있습니다. 예상되는 에러 코드(NOT_FOUND_OPTION)도 함께 검증하면 테스트가 더 정확해집니다.

🧪 에러 코드 검증 추가
 assertThatThrownBy(() ->
     adminProductService.editProductStock(nonExistentProductId, 10, EditStockFlag.INCREASE)
-)
-    .isInstanceOf(BbangleException.class);
+)
+    .isInstanceOf(BbangleException.class)
+    .extracting("errorCode")
+    .isEqualTo(BbangleErrorCode.NOT_FOUND_OPTION);

@kmindev kmindev requested review from CUCU7103 and exjuu February 4, 2026 13:56
Copy link
Collaborator

@CUCU7103 CUCU7103 left a comment

Choose a reason for hiding this comment

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

@kmindev 경민님 고생하셨습니다
궁금한 부분에 대해서는 코멘트 남겼습니다!

@kmindev kmindev merged commit 3ffbd6f into dev Feb 5, 2026
1 check passed
@kmindev kmindev deleted the feature/add_edit_product_stock_feature branch February 5, 2026 13:15
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.

[Feature] (관리자) 상품 재고 증가 기능 구현

4 participants