-
Notifications
You must be signed in to change notification settings - Fork 2
[BE] SISC1-86/87 사용자 베팅 등록/취소 API 구현 #56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The head ref may contain hidden characters: "SISC1-86-BE-\uC0AC\uC6A9\uC790-\uBCA0\uD305-\uB4F1\uB85D-API-\uAD6C\uD604"
Conversation
Walkthrough사용자 베팅 생성/취소 엔드포인트와 컨트롤러 경로 재정렬(/api, /bet-rounds)이 추가되었고, UserBetRequest DTO·UserBetRepository·서비스 로직(post/cancel)·트랜잭션 경계(REQUIRES_NEW) 및 ErrorCode 확장이 도입되었으며 단위 및 트랜잭션 테스트가 추가/확장되었습니다. Changes
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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Comment |
|
|
There was a problem hiding this 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
📒 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 테스트가 주요 시나리오를 적절히 커버합니다.환불 금액, 삭제 호출, 실패 시 부작용 없음을 올바르게 검증합니다.
createPointHistory의originId로userBetId를 전달하는 것이 의도된 동작이므로 변경이 필요 없습니다.Likely an incorrect or invalid review comment.
Summary by CodeRabbit
New Features
Changes
Tests