Skip to content

[Feat/#114] 습관 재도전 시 습관 수행 기간 및 보상 수정 기능 추가 - #115

Merged
guzzang merged 7 commits into
developfrom
feat/#114-retry-habit-patch-duration-reward
Apr 21, 2026
Merged

guzzang merged 7 commits into
developfrom
feat/#114-retry-habit-patch-duration-reward

Conversation

@guzzang

@guzzang guzzang commented Apr 19, 2026 •

Copy link
Copy Markdown
Contributor

📌 관련 이슈

✨ 변경 사항

  • 습관 재도전 시 습관 수행 기간 및 보상을 수정 기능을 추가함

📸 테스트 증명 (필수)

로컬에서 빌드 완료

📚 리뷰어 참고 사항

  • 해당 기능을 구현하며 수행기간이 3일 7일 외 습관에 대한 습관 실패 스케줄러 코드에서 습관의 일일 성공 여부도 수정되어야 할 거 같아 해당 부분도 수정하였습니다.
  • 추후 확장성이나 유지보수를 생각해보니 습관 재도전 시 보상 검증 로직을 Habit 엔티티 내부에 두는게 좋을 거 같아 Habit 엔티티의 retry() (습관 재도전) 로직 안에 두었습니다.

✅ 체크리스트

  • 브랜치 전략(git flow)을 따랐나요?
  • 로컬에서 빌드 및 실행이 정상적으로 되나요?
  • 불필요한 주석이나 더미 코드는 제거했나요?
  • 컨벤션(커밋 메시지, 코드 스타일)을 지켰나요?
  • spotlessApply 실행했나요?

Summary by CodeRabbit

Release Notes

  • New Features

    • Habit retry endpoint now accepts and validates duration and reward parameters, enabling more flexible retry operations.
  • Bug Fixes

    • Failed habits are now properly marked as incomplete, ensuring accurate habit status tracking and preventing data inconsistencies.

@coderabbitai

coderabbitai Bot commented Apr 19, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@guzzang has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 8 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 9 minutes and 8 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 46bf073b-12a9-4042-bfcf-027e60b6e8c9

📥 Commits

Reviewing files that changed from the base of the PR and between c53358c and 3da2f25.

📒 Files selected for processing (4)
  • src/main/java/com/swyp/server/domain/habit/entity/Habit.java
  • src/main/java/com/swyp/server/domain/habit/repository/HabitRepository.java
  • src/main/java/com/swyp/server/domain/habit/service/HabitService.java
  • src/test/java/com/swyp/server/domain/habit/HabitRepositoryTest.java
📝 Walkthrough

Walkthrough

A feature enabling users to modify habit duration and reward during retry attempts. The controller now accepts a request payload with duration and reward parameters, validates them via bean validation, and propagates them through the service and entity layers to update the habit before retry.

Changes

Cohort / File(s) Summary
Controller & DTO
src/main/java/com/swyp/server/domain/habit/controller/HabitController.java, src/main/java/com/swyp/server/domain/habit/dto/HabitRetryRequest.java
Added request payload acceptance with @Valid @RequestBody HabitRetryRequest to the retry endpoint; new DTO record encapsulates duration (required) and reward fields with validation annotations.
Entity & Repository
src/main/java/com/swyp/server/domain/habit/entity/Habit.java, src/main/java/com/swyp/server/domain/habit/repository/HabitRepository.java
Updated Habit.retry() to accept and apply HabitRetryRequest changes to duration and reward; repository bulk update query now also sets isCompleted = false alongside status update.
Service Layer
src/main/java/com/swyp/server/domain/habit/service/HabitService.java
Updated retryFailedHabit() method signature to accept and forward HabitRetryRequest through the service to the entity; minor formatting adjustments in helper methods.
Tests
src/test/java/com/swyp/server/domain/habit/HabitRepositoryTest.java
Extended test coverage to verify isCompleted flag is set to false on failed habit updates; cleaned duplicate imports and reformatted assertions for readability.

Sequence Diagram

sequenceDiagram
    participant Client
    participant HabitController
    participant HabitService
    participant HabitRepository
    participant Habit
    
    Client->>HabitController: POST /habits/{habitId}/retry<br/>(with HabitRetryRequest)
    HabitController->>HabitController: `@Valid` validates request<br/>(duration required)
    HabitController->>HabitService: retryFailedHabit(userId, habitId, request)
    HabitService->>HabitRepository: findByIdAndUserId(habitId, userId)
    HabitRepository-->>HabitService: Habit entity
    HabitService->>Habit: retry(user, request)
    Habit->>Habit: Update duration from request
    Habit->>Habit: Update reward from request
    Habit->>Habit: Determine status based<br/>on user type
    HabitService->>HabitRepository: save(habit)
    HabitRepository-->>HabitService: Success
    HabitService-->>HabitController: ResponseEntity
    HabitController-->>Client: 200 OK
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • PR #113: Extends the earlier habit retry implementation by accepting a HabitRetryRequest payload and propagating it through the service/entity layers.
  • PR #65: Modifies habit reward/status model and related APIs affecting Habit, HabitService, HabitController, and HabitRepository behavior.
  • PR #94: Changes habit domain status and isCompleted field behavior in the Habit entity and repository update queries.

Suggested reviewers

  • yunmi-dev
  • SeHyeonCho

Poem

🐰 A habit retried with care so bright,
New duration, reward—set just right!
Through controller, service, down to the store,
Habits bloom again, ready once more!
Validation guards each change we make,
Data flowing for progress' sake! ✨

🚥 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
Title check ✅ Passed The pull request title clearly summarizes the main feature: adding ability to modify habit duration and rewards when retrying, which directly corresponds to the primary changes across controller, entity, service, and DTO files.
Linked Issues check ✅ Passed The pull request implements all coding requirements from issue #114: adds request body accepting duration and reward modifications [HabitRetryRequest DTO], updates controller to accept this request, modifies service and entity retry methods to process the request, and adjusts failure scheduler logic for non-3/7-day habits.
Out of Scope Changes check ✅ Passed All changes are directly related to issue #114 objectives: the core retry-modification feature, supporting DTO/entity/service updates, test coverage for the feature, and necessary scheduler adjustments for non-3/7-day duration habits fall within scope.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#114-retry-habit-patch-duration-reward

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/main/java/com/swyp/server/domain/habit/service/HabitService.java (1)

230-243: 🛠️ Refactor suggestion | 🟠 Major

Consider centralizing the reward/user-type guard here.

To keep Habit.retry a pure state transition and stay consistent with createHabit (lines 43-49) and updateHabit (lines 196-202), apply the child-requires-non-blank-reward / parent-reward-null guard in this method before calling habit.retry(user, request). See detailed rationale on HabitRetryRequest.java and Habit.java.

♻️ Sketch
     `@Transactional`
     public void retryFailedHabit(Long userId, Long habitId, HabitRetryRequest request) {
         Habit habit =
                 habitRepository
                         .findByIdAndUserIdAndStatus(habitId, userId, RewardStatus.FAIL)
                         .orElseThrow(() -> new CustomException(ErrorCode.HABIT_NOT_FOUND));

         User user =
                 userRepository
                         .findById(userId)
                         .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

-        habit.retry(user, request);
+        String reward = null;
+        if (user.getUserType() == UserType.CHILD) {
+            if (request.reward() == null || request.reward().isBlank()) {
+                throw new CustomException(ErrorCode.HABIT_REWARD_REQUIRED);
+            }
+            reward = request.reward();
+        }
+        habit.retry(user, request.duration(), reward);
     }

(Adjust Habit.retry signature accordingly, or mutate via existing updateDuration/updateReward helpers.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/swyp/server/domain/habit/service/HabitService.java` around
lines 230 - 243, The child/parent reward-type guard should be enforced in
retryFailedHabit before calling Habit.retry: validate the HabitRetryRequest
against the child-requires-non-blank-reward / parent-reward-null rules (same
logic used in createHabit/updateHabit) and throw the appropriate CustomException
if invalid, then call habit.retry(user, request); if Habit.retry currently
contains those guards, refactor them out (or adjust its signature to accept only
already-validated data) and reuse existing helpers like
updateDuration/updateReward to perform any mutations so Habit.retry remains a
pure state transition.
src/main/java/com/swyp/server/domain/habit/entity/Habit.java (2)

93-102: ⚠️ Potential issue | 🔴 Critical

retry() doesn't recalculate expiredAt — retried habits inherit the stale expiry.

expiredAt is set only in the constructor (line 64) and updateDuration (line 74). A failed habit has already passed its expiredAt, so after retry() the habit will either:

  • be immediately picked up again by findExpiredHabits/updateExpiredHabitsStatus (flipping to REWARD_WAITING/COMPLETE on the next scheduler run), or
  • have an entirely wrong duration window that doesn't match the newly-assigned duration.

This defeats the purpose of the "재도전" feature. expiredAt must be recomputed from "now" using the new duration.

🐛 Proposed fix
     public void retry(User user, HabitRetryRequest request) {
         this.duration = request.duration();
         this.reward = request.reward();
         this.status =
                 user.getUserType() == UserType.CHILD
                         ? RewardStatus.REWARD_CHECKING
                         : RewardStatus.IN_PROGRESS;
-
+        this.expiredAt =
+                LocalDateTime.now(ZoneId.of("Asia/Seoul")).plusDays(request.duration().getDays());
+        this.isCompleted = false;
         this.failCount = 0;
     }

Also consider resetting isCompleted — since updateCumulativeFailureHabits now sets isCompleted = false on failure (see HabitRepository.java:75), the habit is already in the correct state today, but being explicit here makes retry() self-contained regardless of how the habit reached FAIL.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/swyp/server/domain/habit/entity/Habit.java` around lines 93
- 102, The retry(User user, HabitRetryRequest request) method currently updates
duration/reward/status/failCount but does not recalculate expiredAt, causing
retried habits to keep a stale expiry; update retry() to set expiredAt = now
plus the new duration (use Instant.now() or the same time source used in the
constructor/updateDuration) so the new window is correct, and also explicitly
reset isCompleted = false (to make retry() self-contained) and any other
time-dependent flags that updateDuration/constructor initialize.

93-102: ⚠️ Potential issue | 🟠 Major

Reward assignment ignores user type; diverges from createHabit/updateHabit.

In HabitService.createHabit (lines 43-49) and updateHabit (lines 196-202), a CHILD user is required to provide a non-blank reward (throwing HABIT_REWARD_REQUIRED), and a PARENT user's reward is forced to null. retry bypasses both rules and blindly assigns request.reward(), which:

  • Lets a PARENT set/overwrite a reward, contradicting issue #114 ("부모 사용자: 재도전 시 실행 기간만 수정 가능").
  • Lets a CHILD persist null/blank reward without error.

Recommend moving the same guard into HabitService.retryFailedHabit (so Habit.retry receives an already-sanitized reward), or inline the same logic here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/swyp/server/domain/habit/entity/Habit.java` around lines 93
- 102, Habit.retry currently assigns request.reward() directly which allows
PARENTs to set/overwrite rewards and allows CHILDREN to persist blank/null
rewards; update the logic to mirror createHabit/updateHabit: if
user.getUserType() == UserType.PARENT then force this.reward = null, otherwise
(CHILD) require a non-blank reward and throw HABIT_REWARD_REQUIRED if missing,
then set duration, status (use RewardStatus.REWARD_CHECKING for CHILD,
IN_PROGRESS for PARENT as already done), and reset failCount; alternatively move
this validation into HabitService.retryFailedHabit so Habit.retry only receives
a pre-sanitized reward.
🧹 Nitpick comments (1)
src/test/java/com/swyp/server/domain/habit/HabitRepositoryTest.java (1)

239-293: Missing test coverage for the new retry flow.

This PR's headline feature — modifying duration and reward on retry, plus the resulting status/failCount/(expected) expiredAt transitions for both CHILD and PARENT users — has no repository/service/controller test. Given the priority (P0) and the logic concerns flagged on Habit.retry, consider adding tests such as:

  • Child retry: new duration and new reward applied; status → REWARD_CHECKING; failCount reset; expiredAt recomputed.
  • Parent retry: duration changed but reward unchanged (or rejected); status → IN_PROGRESS.
  • Retry with null/blank reward for a child → HABIT_REWARD_REQUIRED.
  • Retry with null duration → bean-validation failure.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/swyp/server/domain/habit/HabitRepositoryTest.java` around
lines 239 - 293, Add unit tests covering the new Habit.retry flow: create tests
that invoke Habit.retry(...) (and any repository/service wrapper) for Child and
Parent users and assert the expected field transitions — for Child: duration and
reward updated, status == REWARD_CHECKING, failCount reset to 0, and expiredAt
recomputed; for Parent: duration changed but reward unchanged, status ==
IN_PROGRESS; add negative tests: retry with null/blank reward for Child yields
HABIT_REWARD_REQUIRED, and retry with null duration triggers validation
failure/constraint violation. Reference the Habit.retry method, Habit entity
fields (duration, reward, status, failCount, expiredAt) and the
repository/service method that persists retries (e.g., habitRepository.save /
habitService.retry) when adding these tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/com/swyp/server/domain/habit/dto/HabitRetryRequest.java`:
- Around line 6-7: The HabitRetryRequest currently allows any reward value;
update it to enforce reward validation and mirror existing patterns: annotate
the reward field in HabitRetryRequest with `@NotNull`(message =
"HABIT_REWARD_REQUIRED") and `@NotBlank` (matching HabitRewardUpdateRequest
semantics), and then update HabitService.retryFailedHabit to be user-type-aware
(use the same logic as in HabitService for
HabitCreateRequest/HabitUpdateRequest): for CHILD users require a non-blank
reward (throw/return a validation error with HABIT_REWARD_REQUIRED if missing),
and for PARENT users ignore any supplied reward (do not overwrite Habit.reward
in Habit.retry; leave it null or existing value as appropriate). Reference:
HabitRetryRequest, HabitRewardUpdateRequest, HabitService.retryFailedHabit,
Habit.retry, HabitCreateRequest, HabitUpdateRequest, Habit.java.

---

Outside diff comments:
In `@src/main/java/com/swyp/server/domain/habit/entity/Habit.java`:
- Around line 93-102: The retry(User user, HabitRetryRequest request) method
currently updates duration/reward/status/failCount but does not recalculate
expiredAt, causing retried habits to keep a stale expiry; update retry() to set
expiredAt = now plus the new duration (use Instant.now() or the same time source
used in the constructor/updateDuration) so the new window is correct, and also
explicitly reset isCompleted = false (to make retry() self-contained) and any
other time-dependent flags that updateDuration/constructor initialize.
- Around line 93-102: Habit.retry currently assigns request.reward() directly
which allows PARENTs to set/overwrite rewards and allows CHILDREN to persist
blank/null rewards; update the logic to mirror createHabit/updateHabit: if
user.getUserType() == UserType.PARENT then force this.reward = null, otherwise
(CHILD) require a non-blank reward and throw HABIT_REWARD_REQUIRED if missing,
then set duration, status (use RewardStatus.REWARD_CHECKING for CHILD,
IN_PROGRESS for PARENT as already done), and reset failCount; alternatively move
this validation into HabitService.retryFailedHabit so Habit.retry only receives
a pre-sanitized reward.

In `@src/main/java/com/swyp/server/domain/habit/service/HabitService.java`:
- Around line 230-243: The child/parent reward-type guard should be enforced in
retryFailedHabit before calling Habit.retry: validate the HabitRetryRequest
against the child-requires-non-blank-reward / parent-reward-null rules (same
logic used in createHabit/updateHabit) and throw the appropriate CustomException
if invalid, then call habit.retry(user, request); if Habit.retry currently
contains those guards, refactor them out (or adjust its signature to accept only
already-validated data) and reuse existing helpers like
updateDuration/updateReward to perform any mutations so Habit.retry remains a
pure state transition.

---

Nitpick comments:
In `@src/test/java/com/swyp/server/domain/habit/HabitRepositoryTest.java`:
- Around line 239-293: Add unit tests covering the new Habit.retry flow: create
tests that invoke Habit.retry(...) (and any repository/service wrapper) for
Child and Parent users and assert the expected field transitions — for Child:
duration and reward updated, status == REWARD_CHECKING, failCount reset to 0,
and expiredAt recomputed; for Parent: duration changed but reward unchanged,
status == IN_PROGRESS; add negative tests: retry with null/blank reward for
Child yields HABIT_REWARD_REQUIRED, and retry with null duration triggers
validation failure/constraint violation. Reference the Habit.retry method, Habit
entity fields (duration, reward, status, failCount, expiredAt) and the
repository/service method that persists retries (e.g., habitRepository.save /
habitService.retry) when adding these tests.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6f0a72a6-a43f-4d6c-acc4-10a47f24f08c

📥 Commits

Reviewing files that changed from the base of the PR and between 6bf81be and c53358c.

📒 Files selected for processing (6)
  • src/main/java/com/swyp/server/domain/habit/controller/HabitController.java
  • src/main/java/com/swyp/server/domain/habit/dto/HabitRetryRequest.java
  • src/main/java/com/swyp/server/domain/habit/entity/Habit.java
  • src/main/java/com/swyp/server/domain/habit/repository/HabitRepository.java
  • src/main/java/com/swyp/server/domain/habit/service/HabitService.java
  • src/test/java/com/swyp/server/domain/habit/HabitRepositoryTest.java

Comment on lines +6 to +7
public record HabitRetryRequest(
@NotNull(message = "HABIT_DURATION_REQUIRED") HabitDuration duration, String reward) {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

reward lacks validation and user-type constraints.

Unlike HabitRewardUpdateRequest which uses @NotNull(message = "HABIT_REWARD_REQUIRED"), and unlike HabitCreateRequest/HabitUpdateRequest handling in HabitService which enforces non-blank reward for CHILD users (HABIT_REWARD_REQUIRED) and nulls it for PARENT users, this DTO accepts any reward value without constraints.

Per issue #114, parents should only modify the duration on retry (not the reward), while children must provide a valid reward. The current DTO + Habit.retry implementation lets a parent overwrite reward and lets a child submit null/blank reward, silently persisting it (the reward column has no DB-level constraint either — see Habit.java:38).

Consider aligning with the existing pattern by applying the same user-type-aware reward handling in HabitService.retryFailedHabit (see next comment on Habit.java).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/swyp/server/domain/habit/dto/HabitRetryRequest.java` around
lines 6 - 7, The HabitRetryRequest currently allows any reward value; update it
to enforce reward validation and mirror existing patterns: annotate the reward
field in HabitRetryRequest with `@NotNull`(message = "HABIT_REWARD_REQUIRED") and
`@NotBlank` (matching HabitRewardUpdateRequest semantics), and then update
HabitService.retryFailedHabit to be user-type-aware (use the same logic as in
HabitService for HabitCreateRequest/HabitUpdateRequest): for CHILD users require
a non-blank reward (throw/return a validation error with HABIT_REWARD_REQUIRED
if missing), and for PARENT users ignore any supplied reward (do not overwrite
Habit.reward in Habit.retry; leave it null or existing value as appropriate).
Reference: HabitRetryRequest, HabitRewardUpdateRequest,
HabitService.retryFailedHabit, Habit.retry, HabitCreateRequest,
HabitUpdateRequest, Habit.java.

@guzzang
guzzang merged commit 8e87a24 into develop Apr 21, 2026
1 check passed
@guzzang
guzzang deleted the feat/#114-retry-habit-patch-duration-reward branch April 21, 2026 04:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 습관 재도전 시 습관 실행 기간, 보상 수정 기능 추가

2 participants