타이머 세션 도메인 구현 : feat : 연료 환율 30분=1연료 + 잔여분 이월(pendingMinutes) — 프론트 게…#39
Hidden character warning
Conversation
…스트 모드 정책 미러링, UserFuel.chargeFromStudy + FuelService.chargeFromStudy 추가, TimerSessionService 호출부 변경 #25
|
Warning Review limit reached
More reviews will be available in 15 minutes and 49 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
Walkthrough이 PR은 공부 세션 완료 시 30분 단위로 연료를 환산하고 30분 미만의 잔여분을 누적하는 새로운 충전 메커니즘을 추가합니다. 엔티티 메서드, 서비스 로직, 통합 흐름, 전체 테스트 및 API 문서를 일관되게 갱신합니다. Changes공부 세션 기반 연료 충전
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@SS-Study/src/main/java/com/elipair/spacestudyship/study/fuel/service/FuelService.java`:
- Around line 146-182: The method chargeFromStudy in FuelService currently skips
persisting a FuelTransaction when result.amount() == 0, breaking the idempotency
contract; always persist an idempotency marker transaction (e.g., a
FuelTransaction with amount 0 and TransactionType.CHARGE /
FuelReason.STUDY_SESSION and the same sessionId) after computing result so
subsequent calls to transactionRepository.findById(sessionId) will return it;
update the code path in chargeFromStudy to save this marker (and keep existing
behavior when amount>0), then return the same FuelChargeFromStudyResult as
before.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: fda3e58d-6b3e-41d2-9a17-290f6a63e2cb
📒 Files selected for processing (10)
SS-Study/src/main/java/com/elipair/spacestudyship/study/fuel/dto/FuelChargeFromStudyResult.javaSS-Study/src/main/java/com/elipair/spacestudyship/study/fuel/entity/UserFuel.javaSS-Study/src/main/java/com/elipair/spacestudyship/study/fuel/service/FuelService.javaSS-Study/src/main/java/com/elipair/spacestudyship/study/timer/service/TimerSessionService.javaSS-Study/src/test/java/com/elipair/spacestudyship/study/fuel/entity/UserFuelTest.javaSS-Study/src/test/java/com/elipair/spacestudyship/study/fuel/service/FuelServiceTest.javaSS-Study/src/test/java/com/elipair/spacestudyship/study/timer/service/TimerSessionServiceTest.javaSS-Web/src/test/java/com/elipair/spacestudyship/controller/timer/TimerSessionControllerTest.javadocs/api-specs/03_timer.mddocs/api-specs/04_fuel.md
| @Transactional | ||
| public FuelChargeFromStudyResult chargeFromStudy( | ||
| Long userId, int studyMinutes, String sessionId) { | ||
|
|
||
| if (studyMinutes <= 0) throw new CustomException(ErrorCode.INVALID_INPUT_VALUE); | ||
|
|
||
| Optional<FuelTransaction> existing = transactionRepository.findById(sessionId); | ||
| if (existing.isPresent()) { | ||
| return idempotentReturnFromStudy(existing.get(), userId, sessionId); | ||
| } | ||
|
|
||
| UserFuel fuel = userFuelRepository.findByUserIdForUpdate(userId) | ||
| .orElseThrow(() -> new CustomException(ErrorCode.FUEL_NOT_INITIALIZED)); | ||
|
|
||
| // 락 획득 후 race 재확인 (다른 트랜잭션이 동일 sessionId로 먼저 INSERT했을 가능성) | ||
| Optional<FuelTransaction> raced = transactionRepository.findById(sessionId); | ||
| if (raced.isPresent()) { | ||
| return idempotentReturnFromStudy(raced.get(), userId, sessionId); | ||
| } | ||
|
|
||
| UserFuel.ChargeFromStudyResult result = fuel.chargeFromStudy(studyMinutes); | ||
|
|
||
| if (result.amount() > 0) { | ||
| FuelTransaction tx = FuelTransaction.of( | ||
| sessionId, userId, TransactionType.CHARGE, | ||
| result.amount(), FuelReason.STUDY_SESSION, | ||
| sessionId, fuel.getCurrentFuel()); | ||
| transactionRepository.save(tx); | ||
| } | ||
|
|
||
| log.info("[Fuel] 공부 세션 충전 | userId={}, studyMinutes={}, amount={}, " + | ||
| "pendingMinutes={}, balanceAfter={}, sessionId={}", | ||
| userId, studyMinutes, result.amount(), | ||
| result.newPendingMinutes(), fuel.getCurrentFuel(), sessionId); | ||
| return new FuelChargeFromStudyResult( | ||
| result.amount(), result.newPendingMinutes(), fuel.getCurrentFuel()); | ||
| } |
There was a problem hiding this comment.
amount=0일 때 멱등성 보장이 누락됨
result.amount() == 0인 경우(예: 기존 pendingMinutes가 0이고 studyMinutes가 30 미만) 트랜잭션이 저장되지 않습니다. 동일한 sessionId로 재호출 시 transactionRepository.findById(sessionId)가 빈 결과를 반환하여 pendingMinutes가 중복 누적될 수 있습니다.
현재는 TimerSessionService의 idempotencyKey 기반 검사가 상위 계층에서 보호하고 있지만, 이 메서드의 Javadoc에 명시된 멱등성 계약과 불일치합니다. 향후 직접 호출 시 데이터 불일치가 발생할 수 있습니다.
🛡️ amount=0일 때도 멱등성 마커 트랜잭션 저장 제안
if (result.amount() > 0) {
FuelTransaction tx = FuelTransaction.of(
sessionId, userId, TransactionType.CHARGE,
result.amount(), FuelReason.STUDY_SESSION,
sessionId, fuel.getCurrentFuel());
transactionRepository.save(tx);
+ } else {
+ // amount=0이더라도 멱등성 보장을 위해 마커 트랜잭션 저장
+ FuelTransaction tx = FuelTransaction.of(
+ sessionId, userId, TransactionType.CHARGE,
+ 0, FuelReason.STUDY_SESSION,
+ sessionId, fuel.getCurrentFuel());
+ transactionRepository.save(tx);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @Transactional | |
| public FuelChargeFromStudyResult chargeFromStudy( | |
| Long userId, int studyMinutes, String sessionId) { | |
| if (studyMinutes <= 0) throw new CustomException(ErrorCode.INVALID_INPUT_VALUE); | |
| Optional<FuelTransaction> existing = transactionRepository.findById(sessionId); | |
| if (existing.isPresent()) { | |
| return idempotentReturnFromStudy(existing.get(), userId, sessionId); | |
| } | |
| UserFuel fuel = userFuelRepository.findByUserIdForUpdate(userId) | |
| .orElseThrow(() -> new CustomException(ErrorCode.FUEL_NOT_INITIALIZED)); | |
| // 락 획득 후 race 재확인 (다른 트랜잭션이 동일 sessionId로 먼저 INSERT했을 가능성) | |
| Optional<FuelTransaction> raced = transactionRepository.findById(sessionId); | |
| if (raced.isPresent()) { | |
| return idempotentReturnFromStudy(raced.get(), userId, sessionId); | |
| } | |
| UserFuel.ChargeFromStudyResult result = fuel.chargeFromStudy(studyMinutes); | |
| if (result.amount() > 0) { | |
| FuelTransaction tx = FuelTransaction.of( | |
| sessionId, userId, TransactionType.CHARGE, | |
| result.amount(), FuelReason.STUDY_SESSION, | |
| sessionId, fuel.getCurrentFuel()); | |
| transactionRepository.save(tx); | |
| } | |
| log.info("[Fuel] 공부 세션 충전 | userId={}, studyMinutes={}, amount={}, " + | |
| "pendingMinutes={}, balanceAfter={}, sessionId={}", | |
| userId, studyMinutes, result.amount(), | |
| result.newPendingMinutes(), fuel.getCurrentFuel(), sessionId); | |
| return new FuelChargeFromStudyResult( | |
| result.amount(), result.newPendingMinutes(), fuel.getCurrentFuel()); | |
| } | |
| `@Transactional` | |
| public FuelChargeFromStudyResult chargeFromStudy( | |
| Long userId, int studyMinutes, String sessionId) { | |
| if (studyMinutes <= 0) throw new CustomException(ErrorCode.INVALID_INPUT_VALUE); | |
| Optional<FuelTransaction> existing = transactionRepository.findById(sessionId); | |
| if (existing.isPresent()) { | |
| return idempotentReturnFromStudy(existing.get(), userId, sessionId); | |
| } | |
| UserFuel fuel = userFuelRepository.findByUserIdForUpdate(userId) | |
| .orElseThrow(() -> new CustomException(ErrorCode.FUEL_NOT_INITIALIZED)); | |
| // 락 획득 후 race 재확인 (다른 트랜잭션이 동일 sessionId로 먼저 INSERT했을 가능성) | |
| Optional<FuelTransaction> raced = transactionRepository.findById(sessionId); | |
| if (raced.isPresent()) { | |
| return idempotentReturnFromStudy(raced.get(), userId, sessionId); | |
| } | |
| UserFuel.ChargeFromStudyResult result = fuel.chargeFromStudy(studyMinutes); | |
| if (result.amount() > 0) { | |
| FuelTransaction tx = FuelTransaction.of( | |
| sessionId, userId, TransactionType.CHARGE, | |
| result.amount(), FuelReason.STUDY_SESSION, | |
| sessionId, fuel.getCurrentFuel()); | |
| transactionRepository.save(tx); | |
| } else { | |
| // amount=0이더라도 멱등성 보장을 위해 마커 트랜잭션 저장 | |
| FuelTransaction tx = FuelTransaction.of( | |
| sessionId, userId, TransactionType.CHARGE, | |
| 0, FuelReason.STUDY_SESSION, | |
| sessionId, fuel.getCurrentFuel()); | |
| transactionRepository.save(tx); | |
| } | |
| log.info("[Fuel] 공부 세션 충전 | userId={}, studyMinutes={}, amount={}, " + | |
| "pendingMinutes={}, balanceAfter={}, sessionId={}", | |
| userId, studyMinutes, result.amount(), | |
| result.newPendingMinutes(), fuel.getCurrentFuel(), sessionId); | |
| return new FuelChargeFromStudyResult( | |
| result.amount(), result.newPendingMinutes(), fuel.getCurrentFuel()); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@SS-Study/src/main/java/com/elipair/spacestudyship/study/fuel/service/FuelService.java`
around lines 146 - 182, The method chargeFromStudy in FuelService currently
skips persisting a FuelTransaction when result.amount() == 0, breaking the
idempotency contract; always persist an idempotency marker transaction (e.g., a
FuelTransaction with amount 0 and TransactionType.CHARGE /
FuelReason.STUDY_SESSION and the same sessionId) after computing result so
subsequent calls to transactionRepository.findById(sessionId) will return it;
update the code path in chargeFromStudy to save this marker (and keep existing
behavior when amount>0), then return the same FuelChargeFromStudyResult as
before.
…이스는 호출자가 sessionId 유일성 보장 책임) #25
🗑️ Preview 환경 삭제 완료!
다시 배포하려면: 🤖 이 댓글은 PR/Issue Preview 시스템에 의해 자동 생성되었습니다. |
…스트 모드 정책 미러링, UserFuel.chargeFromStudy + FuelService.chargeFromStudy 추가, TimerSessionService 호출부 변경 #25
✨ 변경 사항
✅ 테스트
Summary by CodeRabbit
Release Notes
New Features
Documentation
pendingMinutes필드 설명 명확화Tests