Skip to content

Conversation

@otter2023
Copy link
Member

@otter2023 otter2023 commented Oct 13, 2025

Summary by CodeRabbit

  • New Features

    • 사용자 베팅 생성/취소 API 추가: POST /api/user-bets, DELETE /api/user-bets/{id}
    • 사용자 베팅 요청 DTO에 유효성 검증(라운드, 옵션 필수; 최소 베팅점수; 무료 베팅 지원)
  • Changes

    • 베팅 라운드 조회 경로 변경: GET /bet-rounds/{scope}, GET /bet-rounds/history
    • 오류 코드 및 메시지 확장(중복, 시간제한, 미존재, 마감 등)
    • 포인트 내역 처리 트랜잭션 분리로 안정성 향상
  • Tests

    • 서비스 단위 및 트랜잭션 통합 테스트 추가/확장 (베팅/취소 시나리오 검증)

@coderabbitai
Copy link

coderabbitai bot commented Oct 13, 2025

Walkthrough

사용자 베팅 생성/취소 엔드포인트와 컨트롤러 경로 재정렬(/api, /bet-rounds)이 추가되었고, UserBetRequest DTO·UserBetRepository·서비스 로직(post/cancel)·트랜잭션 경계(REQUIRES_NEW) 및 ErrorCode 확장이 도입되었으며 단위 및 트랜잭션 테스트가 추가/확장되었습니다.

Changes

Cohort / File(s) Summary
Controller: REST 경로/엔드포인트 추가
backend/src/main/java/.../betting/controller/BettingController.java
클래스 레벨에 @Validated·@RequestMapping("/api") 추가. GET 경로 /bet-round/{scope}/bet-rounds/{scope}, /bet-round/history/bet-rounds/history로 변경. POST /api/user-bets(생성) 및 DELETE /api/user-bets/{userBetId}(취소) 추가, @AuthenticationPrincipal·@Valid 적용.
DTO: 사용자 베팅 요청
backend/src/main/java/.../betting/dto/UserBetRequest.java
UserBetRequest 신설: roundId: UUID(@NotNull), option: BetOption(@NotNull), isFree: boolean(JSON isFree), stakePoints: Integer(@Min(10)). Lombok 빌더/생성자/게터 적용.
Entity: Lombok 보일러플레이트 추가
backend/src/main/java/.../betting/entity/UserBet.java
@Getter, @Builder, @NoArgsConstructor, @AllArgsConstructor 추가 (보일러플레이트 생성).
Repository: 사용자 베팅
backend/src/main/java/.../betting/repository/UserBetRepository.java
UserBetRepository 신설(JpaRepository<UserBet, UUID>). 메서드: existsByRoundAndUserId(BetRound, UUID), findByUserBetIdAndUserId(UUID, UUID).
Service: 베팅 생성/취소 및 트랜잭션
backend/src/main/java/.../betting/service/BettingService.java
postUserBet(UUID userId, UserBetRequest)cancelUserBet(UUID userId, UUID userBetId) 추가. 라운드 조회, 중복/시간/락 검증, 무료/유료 포인트 처리, PointHistoryService 사용, UserBetRepository 사용, 메서드에 @Transactional.
공통 예외 코드 확장
backend/src/main/java/.../common/exception/ErrorCode.java
BET_ROUND_NOT_FOUND, BET_DUPLICATE, BET_TIME_INVALID, BET_NOT_FOUND, BET_ROUND_CLOSED 추가(기존 enum 항목 이어짐).
포인트 이력 트랜잭션 경계
backend/src/main/java/.../point/service/PointHistoryService.java
createPointHistory@Transactional(propagation = Propagation.REQUIRES_NEW) 적용(새 트랜잭션 경계).
테스트: 서비스 단위 테스트 확장
backend/src/test/java/.../betting/service/BettingServiceTest.java
UserBetRepository·PointHistoryService Mock 주입 반영. 유료/무료/중복/시간/미존재/락 이후/환불 등 시나리오 단위 테스트 추가 및 검증 확장.
테스트: 트랜잭션 동작 검증
backend/src/test/java/.../betting/service/BettingServiceTransactionalTest.java
스프링 통합 스타일 테스트 추가: PointHistory 별도 트랜잭션 저장(REQ_NEW) 검증, userBet 저장 예외 유도로 외부 트랜잭션 롤백 시나리오 검증.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as 사용자
  participant C as BettingController
  participant S as BettingService
  participant BR as BetRoundRepository
  participant UBR as UserBetRepository
  participant PH as PointHistoryService

  U->>C: POST /api/user-bets (UserBetRequest)
  C->>S: postUserBet(userId, req)
  S->>BR: findById(req.roundId)
  BR-->>S: BetRound / 없음
  alt invalid (not found / time / duplicate)
    S-->>C: ApiException(ErrorCode.*)
    C-->>U: 4xx
  else valid
    opt paid bet
      S->>PH: createPointHistory(-stakePoints, ORIGIN_BET)
      PH-->>S: PointHistory
    end
    S->>UBR: save(UserBet)
    UBR-->>S: UserBet
    S-->>C: UserBet
    C-->>U: 200 OK
  end
Loading
sequenceDiagram
  autonumber
  actor U as 사용자
  participant C as BettingController
  participant S as BettingService
  participant UBR as UserBetRepository
  participant PH as PointHistoryService

  U->>C: DELETE /api/user-bets/{userBetId}
  C->>S: cancelUserBet(userId, userBetId)
  S->>UBR: findByUserBetIdAndUserId(id, userId)
  UBR-->>S: UserBet / 없음
  alt not found / round closed
    S-->>C: ApiException(ErrorCode.*)
    C-->>U: 4xx
  else ok
    opt paid bet
      S->>PH: createPointHistory(+stakePoints, ORIGIN_BET_CANCEL)
      PH-->>S: PointHistory
    end
    S->>UBR: delete(UserBet)
    S-->>C: Void
    C-->>U: 204 No Content
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • msciki7

Poem

나는 토끼, 당근 주머니 가득 들고 와서 말하네,
새 베팅길이 뱅글뱅글, 라운드들 정렬됐네.
무료일까 유료일까, 선택은 네 발자국 따라,
포인트는 별도 트랜잭션에 쏙—안전 배송! 🥕✨

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 (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed 제목이 사용자 베팅 등록 및 취소 API 구현이라는 주요 변경점을 명확히 요약하고 있으며, 티켓 번호와 [BE] 접두사를 포함해 추적 용도로도 적절하므로 간결하고 구체적입니다.
✨ 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 SISC1-86-BE-사용자-베팅-등록-API-구현

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7126ff3 and eb10c20.

📒 Files selected for processing (1)
  • backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java

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

@github-actions
Copy link

github-actions bot commented Oct 13, 2025

⚠️ No diff coverage results

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

🧹 Nitpick comments (6)
backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTransactionalTest.java (1)

60-65: 무료 베팅 테스트 케이스 추가 권장

현재 테스트는 유료 베팅(free=false, stakePoints=100) 시나리오만 다룹니다. 무료 베팅 시나리오에서 포인트 히스토리가 호출되지 않아야 함을 검증하는 테스트 케이스를 추가하는 것이 좋습니다.

backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java (2)

137-139: 시간 비교 로직 일관성 개선 권장

Line 97에서는 now.isBefore()now.isAfter()를 사용하는 반면, Line 137에서는 now.isAfter()만 사용합니다. 코드 일관성을 위해 동일한 패턴을 사용하는 것이 좋습니다.

-    if (LocalDateTime.now().isAfter(betRound.getLockAt())){
+    LocalDateTime now = LocalDateTime.now();
+    if (now.isAfter(betRound.getLockAt())){
         throw new CustomException(ErrorCode.BET_ROUND_CLOSED);
     }

147-147: 포인트 히스토리 originId 일관성 확보
베팅 생성(create) 시 userBetRequest.getRoundId()(라인 107), 취소(cancel) 시 userBet.getUserBetId()(라인 147)를 사용 중입니다. 두 경우 동일한 식별자를 사용하거나 역할을 명확히 문서화하세요.

backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTest.java (3)

237-259: 시간 기반 테스트의 안정성 개선을 권장합니다.

LocalDateTime.now()를 사용하면 시간 경계 부근에서 테스트가 불안정해질 수 있습니다.

고정된 시간을 사용하도록 리팩토링하는 것을 권장합니다:

 private BetRound openRoundNow() {
-    LocalDateTime now = LocalDateTime.now();
+    LocalDateTime fixedTime = LocalDateTime.of(2025, 10, 13, 12, 0);
     return BetRound.builder()
             .betRoundID(roundId)
             .scope(Scope.DAILY)
             .status(true)
             .title("OPEN")
-            .openAt(now.minusMinutes(1))
-            .lockAt(now.plusMinutes(10))
+            .openAt(fixedTime.minusMinutes(1))
+            .lockAt(fixedTime.plusMinutes(10))
             .build();
 }

 private BetRound closedRoundNow() {
-    LocalDateTime now = LocalDateTime.now();
+    LocalDateTime fixedTime = LocalDateTime.of(2025, 10, 13, 12, 0);
     return BetRound.builder()
             .betRoundID(roundId)
             .scope(Scope.DAILY)
             .status(true)
             .title("CLOSED")
-            .openAt(now.minusMinutes(10))
-            .lockAt(now.minusMinutes(1))
+            .openAt(fixedTime.minusMinutes(10))
+            .lockAt(fixedTime.minusMinutes(1))
             .build();
 }

270-277: 무료 베팅 시 stakePoints 무시 로직 검증을 추가하세요.

Line 275의 주석에서 stakePoints가 무시되어야 한다고 명시하고 있지만, 이를 명시적으로 검증하는 테스트가 없습니다.

postUserBet_free_success 테스트에 다음 검증을 추가하는 것을 고려하세요:

@Test
@DisplayName("postUserBet: 무료 베팅 성공 → 포인트 차감 호출 안함, stake=0")
void postUserBet_free_success() {
    BetRound round = openRoundNow();

    when(betRoundRepository.findById(roundId)).thenReturn(Optional.of(round));
    when(userBetRepository.existsByRoundAndUserId(round, userId)).thenReturn(false);
    when(userBetRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

    UserBetRequest req = freeReq(); // stakePoints=999 전달

    UserBet result = bettingService.postUserBet(userId, req);

    assertThat(result.isFree()).isTrue();
    assertThat(result.getStakePoints()).isZero(); // ✓ 이미 검증됨
    // 추가: 입력된 999가 무시되고 0으로 설정되었음을 명시적으로 확인
    assertThat(req.getStakePoints()).isEqualTo(999); // 요청 객체는 변경되지 않음
    assertThat(result.getStakePoints()).isNotEqualTo(req.getStakePoints()); // 결과는 다름

    verify(pointHistoryService, never()).createPointHistory(any(), anyInt(), any(), any(), any());
    verify(userBetRepository).save(any(UserBet.class));
}

279-364: postUserBet 테스트 커버리지가 우수합니다.

성공 케이스(유료/무료)와 실패 케이스(라운드 없음, 중복, 시간 무효)를 모두 검증하고 있으며, 포인트 서비스 호출 여부를 적절히 확인합니다.

더 엄격한 검증을 위해 저장되는 UserBet 객체의 필드를 명시적으로 확인하는 것을 고려하세요:

@Test
@DisplayName("postUserBet: 유료 베팅 성공 → 포인트 차감 호출 + 저장")
void postUserBet_paid_success() {
    BetRound round = openRoundNow();

    when(betRoundRepository.findById(roundId)).thenReturn(Optional.of(round));
    when(userBetRepository.existsByRoundAndUserId(round, userId)).thenReturn(false);
    when(userBetRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

    UserBetRequest req = paidReq(100);

    UserBet result = bettingService.postUserBet(userId, req);

    assertThat(result.getStakePoints()).isEqualTo(100);
    assertThat(result.isFree()).isFalse();
    // 추가 검증
    assertThat(result.getUserId()).isEqualTo(userId);
    assertThat(result.getRound()).isEqualTo(round);
    assertThat(result.getOption()).isEqualTo(BetOption.RISE);

    verify(pointHistoryService).createPointHistory(
            eq(userId), eq(-100),
            eq(PointReason.BETTING),
            eq(PointOrigin.BETTING),
            eq(roundId)
    );
    
    // ArgumentCaptor를 사용한 더 엄격한 검증
    ArgumentCaptor<UserBet> captor = ArgumentCaptor.forClass(UserBet.class);
    verify(userBetRepository).save(captor.capture());
    UserBet saved = captor.getValue();
    assertThat(saved.getStakePoints()).isEqualTo(100);
    assertThat(saved.isFree()).isFalse();
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d3114b8 and 7126ff3.

📒 Files selected for processing (9)
  • backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java (3 hunks)
  • backend/src/main/java/org/sejongisc/backend/betting/dto/UserBetRequest.java (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/betting/entity/UserBet.java (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/betting/repository/UserBetRepository.java (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java (2 hunks)
  • backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/point/service/PointHistoryService.java (2 hunks)
  • backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTest.java (3 hunks)
  • backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTransactionalTest.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
backend/src/main/java/org/sejongisc/backend/betting/entity/UserBet.java (4)
backend/src/main/java/org/sejongisc/backend/template/dto/TemplateResponse.java (1)
  • Builder (10-15)
backend/src/main/java/org/sejongisc/backend/betting/dto/UserBetRequest.java (1)
  • Getter (14-29)
backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java (1)
  • Entity (11-59)
backend/src/main/java/org/sejongisc/backend/betting/entity/Stock.java (1)
  • Entity (11-33)
backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java (1)
backend/src/main/java/org/sejongisc/backend/common/auth/springsecurity/CustomUserDetailsService.java (1)
  • RequiredArgsConstructor (14-30)
backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java (1)
backend/src/main/java/org/sejongisc/backend/point/service/PointHistoryService.java (1)
  • Service (33-104)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Backend Gate
🔇 Additional comments (10)
backend/src/main/java/org/sejongisc/backend/betting/dto/UserBetRequest.java (1)

24-25: @JsonProperty 사용이 적절합니다

free 필드를 JSON의 isFree로 매핑하는 것은 명명 규칙을 명확히 하고 클라이언트와의 계약을 개선합니다.

backend/src/main/java/org/sejongisc/backend/betting/entity/UserBet.java (1)

4-7: Lombok 어노테이션 추가가 적절합니다

엔티티에 @Getter, @Builder, @NoArgsConstructor, @AllArgsConstructor를 추가하여 보일러플레이트 코드를 제거하고, 서비스 레이어와 테스트 코드에서 사용하기 편리하게 개선되었습니다. 코드베이스의 다른 엔티티들(BetRound, Stock)과도 일관성 있는 패턴입니다.

Also applies to: 13-14

backend/src/main/java/org/sejongisc/backend/betting/repository/UserBetRepository.java (1)

10-12: LGTM!

리포지토리 메서드들이 Spring Data JPA 명명 규칙을 따르고 있으며, existsByRoundAndUserId는 중복 베팅 방지에, findByUserBetIdAndUserId는 소유권 검증에 적절히 사용되고 있습니다.

backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java (1)

56-61: LGTM!

베팅 관련 에러 코드들이 명확한 메시지와 적절한 HTTP 상태 코드로 추가되었습니다. 각 에러 시나리오(라운드 미존재, 중복 베팅, 시간 제약, 베팅 미존재, 라운드 마감)가 잘 표현되어 있습니다.

backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java (1)

86-128: 전반적인 로직이 견고합니다

postUserBet 메서드가 라운드 검증, 중복 방지, 시간 제약, 포인트 처리 등을 체계적으로 수행하고 있습니다. 트랜잭션 경계도 적절히 설정되어 있습니다.

backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java (3)

51-59: LGTM!

새로운 postUserBet 엔드포인트가 인증(@AuthenticationPrincipal)과 검증(@Valid)을 적절히 적용하고 있으며, 비즈니스 로직은 서비스 레이어에 위임하여 관심사의 분리가 잘 되어 있습니다.


61-68: LGTM!

cancelUserBet 엔드포인트가 DELETE 메서드에 적합한 204 No Content를 반환하고 있으며, 인증 처리가 올바르게 구현되어 있습니다.


31-31: API 경로 명명 규칙 개선이 적절합니다

기존의 단수형 /bet-round에서 복수형 /bet-rounds로 변경하여 RESTful API 명명 규칙을 따르도록 개선되었습니다.

Also applies to: 44-44

backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTest.java (2)

42-59: 테스트 설정이 올바르게 구성되었습니다.

새로운 의존성(UserBetRepository, PointHistoryService)이 정확하게 모킹되고 주입되었으며, 테스트 격리가 @beforeeach를 통해 적절히 유지됩니다.


366-434: cancelUserBet 테스트가 주요 시나리오를 적절히 커버합니다.

환불 금액, 삭제 호출, 실패 시 부작용 없음을 올바르게 검증합니다. createPointHistoryoriginIduserBetId를 전달하는 것이 의도된 동작이므로 변경이 필요 없습니다.

Likely an incorrect or invalid review comment.

discipline24
discipline24 previously approved these changes Oct 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants