Skip to content

[Feat/#110] 습관 재도전 기능 구현 - #113

Merged
guzzang merged 9 commits into
developfrom
feat/#110-retry-habit
Apr 18, 2026
Merged

guzzang merged 9 commits into
developfrom
feat/#110-retry-habit

Conversation

@guzzang

@guzzang guzzang commented Apr 15, 2026 •

Copy link
Copy Markdown
Contributor

📌 관련 이슈

✨ 변경 사항

  • 재도전 습관 조회 및 삭제 기능 습관 재도전 기능 추가
  • 습관 실패 시에도 습관 상태 변경되도록 습관 스케줄러 기능 추가

📸 테스트 증명 (필수)

image

📚 리뷰어 참고 사항

  • 14~99일짜리 습관은 기간 중 10일마다 하루는 습관을 체크하지 못해도 습관 기간이 지속된다는 정책을 해당 습관들은 10일마다 실패 횟수가 초기화 되는 식으로 구현을 하였는데, JPA의 JPQL 문법으론 구현이 어려워 nativeQuery를 사용하였습니다. 그래서 기존의 JPQL 문법을 사용하는 쿼리들은 Habit 엔티티를 대상으로 쿼리를 날리지만, 해당 쿼리는 habits DB 테이블을 향해 쿼리를 날리는 구조로 되어있어 기존 쿼리들과 형태가 약간 다릅니다.
  • 습관 스케줄러 기능들을 시간별로 나누는 것이 크게 의미 없다고 판단되어 모두 00시에 동작되도록 스케줄러 코드를 변경하였습니다.

✅ 체크리스트

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

Summary by CodeRabbit

  • New Features
    • View a dedicated list of failed habits.
    • Retry a failed habit to return it to active progress.
    • Automatic failure tracking that marks habits as failed based on completion patterns and resets failure counters periodically.
  • Behavior Changes
    • Failed habits are excluded from reward lookups and related reward displays.

@guzzang guzzang linked an issue Apr 15, 2026 that may be closed by this pull request
1 of 8 tasks
@coderabbitai

coderabbitai Bot commented Apr 15, 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 30 minutes and 49 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 30 minutes and 49 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: a6500c40-9d3c-4518-bbf5-dbcdd8a145cc

📥 Commits

Reviewing files that changed from the base of the PR and between e3917ec and 0603d96.

📒 Files selected for processing (1)
  • src/main/java/com/swyp/server/domain/habit/repository/HabitRepository.java
📝 Walkthrough

Walkthrough

Adds failed-habit tracking and retry functionality: new endpoints to list and retry failed habits, DTOs for failed-habit responses, a persisted failCount on Habit with a retry(User) transition, FAIL status, repository bulk-update queries and reset, scheduler changes to update/reset failures, service methods, and repository tests.

Changes

Cohort / File(s) Summary
API Endpoints
src/main/java/com/swyp/server/domain/habit/controller/HabitController.java
Added GET /api/v1/habits/failed and PATCH /api/v1/habits/failed/{habitId}/status/in-progress handlers with Swagger annotations.
Response DTOs
src/main/java/com/swyp/server/domain/habit/dto/FailedHabitListResponse.java, src/main/java/com/swyp/server/domain/habit/dto/FailedHabitResponse.java
New records and factory methods mapping Habit -> FailedHabitResponse; reward nulled for parent users; wrapper list record.
Entity & Status
src/main/java/com/swyp/server/domain/habit/entity/Habit.java, src/main/java/com/swyp/server/domain/habit/entity/RewardStatus.java
Added non-null failCount field and retry(User) method resetting failCount and adjusting status per user type; added FAIL enum constant.
Repository Layer
src/main/java/com/swyp/server/domain/habit/repository/HabitRepository.java
Added findByIdAndUserIdAndStatus, findAllFailedHabitsByUserId, modified multi-status query to require statuses, added bulk @Modifying updates (immediate/cumulative failure, increment failCount) and native resetFailCount.
Service Layer
src/main/java/com/swyp/server/domain/habit/service/HabitService.java
Added getFailedHabits(userId) and retryFailedHabit(userId, habitId) with existence checks; adjusted reward/status search logic to handle FAIL and empty-status cases.
Scheduler
src/main/java/com/swyp/server/domain/habit/service/HabitScheduler.java
checkExpiredHabits() now invokes fail-count recalculation, immediate/cumulative failure updates, and resetFailCount(now); removed separate resetDailyHabits() scheduled method (behavior integrated).
Repository Tests
src/test/java/com/swyp/server/domain/habit/HabitRepositoryTest.java
Converted to @SpringBootTest, added tests for failCount increment, immediate (3/7-day) failures, cumulative failures, and periodic reset; uses JdbcTemplate and HabitService in tests.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Controller as HabitController
    participant Service as HabitService
    participant Repository as HabitRepository
    participant DB as Database

    rect rgba(100,200,150,0.5)
    Note over Client,DB: List Failed Habits
    Client->>Controller: GET /api/v1/habits/failed
    Controller->>Service: getFailedHabits(userId)
    Service->>Repository: findAllFailedHabitsByUserId(userId)
    Repository->>DB: SELECT habits WHERE status='FAIL'
    DB-->>Repository: List<Habit>
    Repository-->>Service: List<Habit>
    Service->>Service: map to FailedHabitListResponse
    Service-->>Controller: ApiResponse<FailedHabitListResponse>
    Controller-->>Client: 200 OK
    end

    rect rgba(150,150,200,0.5)
    Note over Client,DB: Retry Failed Habit
    Client->>Controller: PATCH /api/v1/habits/failed/{habitId}/status/in-progress
    Controller->>Service: retryFailedHabit(userId, habitId)
    Service->>Repository: findByIdAndUserIdAndStatus(habitId,userId,FAIL)
    Repository->>DB: SELECT habit WHERE id,user,status=FAIL
    DB-->>Repository: Habit
    Repository-->>Service: Habit
    Service->>Service: load User -> habit.retry(user) (reset failCount, set status)
    Service->>Repository: save updated Habit
    Repository->>DB: UPDATE habit
    DB-->>Repository: success
    Service-->>Controller: ApiResponse<Void>
    Controller-->>Client: 200 OK
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • yunmi-dev
  • SeHyeonCho

Poem

🐰 I counted the hops that fell from the trail,
A failCount tucked under each tiny tail.
With a wiggle and nudge I set them to try—
Back into progress beneath bright sky.
Hop on, small habit, you're ready to fly! 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.41% 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 The title 'Feat/#110 retry habit' directly corresponds to the primary feature being implemented—the retry habit functionality described in linked issue #110.
Linked Issues check ✅ Passed The PR implements all coding requirements from issue #110: failed habits retrieval endpoint, retry mechanism with correct status handling for child/parent users, filtering failed habits from reward tabs, and scheduler updates for habit state transitions.
Out of Scope Changes check ✅ Passed All changes align with issue #110 requirements—new endpoints for failed habit retrieval/retry, status/failure tracking, scheduler consolidation, and test coverage. No unrelated modifications detected.

✏️ 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/#110-retry-habit

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

🤖 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/entity/Habit.java`:
- Around line 49-50: The new non-nullable field Habit.failCount (annotated
`@Column`(nullable = false)) will fail on existing databases when Hibernate
ddl-auto=update tries to add the column without backfilling; confirm your
deployment target is a fresh DB or else create a migration/backfill: either run
a manual ALTER TABLE to add fail_count with a DEFAULT 0 and update existing
rows, or add a Flyway/Liquibase migration that alters the habits table and sets
fail_count = 0 for existing records (or temporarily make the column
nullable/defaulted then flip to NOT NULL), and ensure the Builder that
initializes failCount to 0 remains for new entities.

In `@src/main/java/com/swyp/server/domain/habit/entity/RewardStatus.java`:
- Around line 13-14: Remove the FAIL enum constant from RewardStatus so the
reward-tab filter cannot bind to a "FAIL" value; update any code that relied on
RewardStatus.FAIL (notably HabitService.determineSearchStatuses()) to use a
different status or handle failed-habit logic separately (e.g., use a separate
enum or explicit check) so failed habits are never returned for the reward tab.
Ensure RewardStatus only contains values intended for the rewards filter (e.g.,
ALL and any other allowed states) and adjust
HabitService.determineSearchStatuses() to map incoming filter params to the
appropriate allowed RewardStatus values or alternative handling.

In `@src/main/java/com/swyp/server/domain/habit/repository/HabitRepository.java`:
- Around line 71-77: The JPQL in updateCumulativeFailureHabits() currently sets
status='FAIL' for habits with failCount>1 without checking today's completion;
modify the query to include a check that h.isCompleted = false so only habits
not completed in the current cycle are failed (keep the existing conditions on
h.failCount, h.status, and h.duration). Locate the method
updateCumulativeFailureHabits() in HabitRepository and add the isCompleted
condition to the WHERE clause to mirror updateHabitFailCount() behavior.
- Around line 27-29: The query method findAllFailedHabitsByUserId currently
returns all habits with status 'FAIL' regardless of expiration; update its JPQL
to add an "AND h.expiredAt >= :now" predicate (matching the pattern used in
other repository queries) and add a new `@Param`("now") parameter, then update the
service method HabitService.getFailedHabits() to pass the current
Instant/LocalDateTime as the now parameter when calling
findAllFailedHabitsByUserId so only still-retryable failures within the
expiration window are returned.

In `@src/main/java/com/swyp/server/domain/habit/service/HabitScheduler.java`:
- Around line 33-39: The current order applies failure escalations before the
10-day reset, causing habits to be marked FAIL prematurely; in HabitScheduler
change the sequence so habitRepository.resetFailCount(now) runs before
habitRepository.updateHabitFailCount(), updateImmediateFailureHabits(), and
updateCumulativeFailureHabits() (or alternatively incorporate the 10-day reset
condition into the failure update queries), ensuring updateHabitFailCount(),
updateImmediateFailureHabits(), and updateCumulativeFailureHabits() operate on
already-reset fail counts.

In `@src/main/java/com/swyp/server/domain/habit/service/HabitService.java`:
- Around line 229-236: The retryFailedHabit method must only operate on habits
that are actually in FAIL and within the configured retry window, and it must
perform a full state reset (clear failCount and relevant timestamps) and set the
correct post-retry status depending on parent/child semantics. Update
retryFailedHabit to: 1) verify habit.getRewardStatus() == RewardStatus.FAIL and
check a retry-window predicate (e.g., habit.isWithinRetryPeriod() or compare
habit.getFailedAt()/habit.getConfiguredPeriod()), throwing
CustomException(ErrorCode.INVALID_HABIT_STATE) if not; 2) reset state fields
like habit.setFailCount(0) and clear/refresh any failure timestamps; and 3) set
reward status appropriately — for child habits use RewardStatus.REWARD_CHECKING
(so child shows 진행중 while parent shows 보상 확인중), otherwise set
RewardStatus.IN_PROGRESS for owning/parent habits; keep these updates inside the
existing `@Transactional` retryFailedHabit method and use the existing Habit
methods (or add small helpers on Habit) to perform the resets.
🪄 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: 527137d1-031e-4f7b-b816-7cca613f8215

📥 Commits

Reviewing files that changed from the base of the PR and between 88ebd2c and add6098.

📒 Files selected for processing (9)
  • src/main/java/com/swyp/server/domain/habit/controller/HabitController.java
  • src/main/java/com/swyp/server/domain/habit/dto/FailedHabitListResponse.java
  • src/main/java/com/swyp/server/domain/habit/dto/FailedHabitResponse.java
  • src/main/java/com/swyp/server/domain/habit/entity/Habit.java
  • src/main/java/com/swyp/server/domain/habit/entity/RewardStatus.java
  • src/main/java/com/swyp/server/domain/habit/repository/HabitRepository.java
  • src/main/java/com/swyp/server/domain/habit/service/HabitScheduler.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 +49 to +50
@Column(nullable = false)
private int failCount;

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect migration/changelog files for the new habits.fail_count column and a backfill/default.
fd -HI 'V*__*.sql' .
fd -HI '*changelog*' .
fd -HI '*.sql' .
fd -HI '*.xml' .
fd -HI '*.yaml' .
fd -HI '*.yml' .

rg -n -C2 'fail_count|ALTER TABLE habits|CREATE TABLE habits|UPDATE habits SET .*fail_count' .

Repository: swyp-4th-team7/server

Length of output: 355


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for migration/changelog files with corrected syntax
echo "=== Searching for migration files ==="
fd --glob "**/V*.sql" . 2>/dev/null || true
fd --glob "**/migrations/**" . 2>/dev/null || true
fd --glob "**/*changelog*" . 2>/dev/null || true

echo ""
echo "=== Searching for SQL files ==="
find . -name "*.sql" -type f 2>/dev/null | head -20

echo ""
echo "=== Searching for XML changelog files ==="
find . -name "*.xml" -type f 2>/dev/null | head -20

echo ""
echo "=== Searching for fail_count references ==="
rg -i 'fail_count' --type sql --type xml --type yaml . 2>/dev/null || true

echo ""
echo "=== Searching for habits table modifications ==="
rg -i 'ALTER TABLE.*habits|CREATE TABLE.*habits|UPDATE.*habits' --type sql . 2>/dev/null || true

Repository: swyp-4th-team7/server

Length of output: 273


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Project structure (top-level directories) ==="
ls -la | grep -E '^d'

echo ""
echo "=== Checking for db/migration directories ==="
find . -type d -name "db" -o -type d -name "migration*" -o -type d -name "sql" 2>/dev/null

echo ""
echo "=== Checking pom.xml for migration tool configuration ==="
if [ -f pom.xml ]; then
  rg -A5 -B5 'flyway|liquibase|migration' pom.xml
fi

echo ""
echo "=== Looking for resources directory ==="
find . -type d -name "resources" | head -10

echo ""
echo "=== List all files in src/main/resources if it exists ==="
if [ -d "src/main/resources" ]; then
  find src/main/resources -type f | head -20
fi

echo ""
echo "=== Check if fail_count exists in any migration-related files ==="
find . -type f \( -name "*.sql" -o -name "*.xml" -o -name "*.yaml" -o -name "*.yml" \) 2>/dev/null | xargs grep -l "fail_count" 2>/dev/null || echo "No fail_count found in any files"

Repository: swyp-4th-team7/server

Length of output: 953


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Checking build.gradle for migration tool ==="
if [ -f build.gradle ]; then
  cat build.gradle | grep -A10 -B2 -i "flyway\|liquibase\|migration" || echo "No migration tool found in build.gradle"
fi

echo ""
echo "=== Git diff summary to see what changed ==="
git diff --stat HEAD~1 HEAD 2>/dev/null || git show --stat 2>/dev/null || echo "Cannot determine git diff"

echo ""
echo "=== Check if Habit.java changed ==="
git log -1 --oneline src/main/java/com/swyp/server/domain/habit/entity/Habit.java 2>/dev/null || true

echo ""
echo "=== Looking for any database schema files ==="
find . -type f \( -name "*.sql" -o -name "schema*" -o -name "*init*" \) 2>/dev/null | grep -v ".git" | head -20

echo ""
echo "=== Check if there's any database initialization in application.yml ==="
cat src/main/resources/application.yml | grep -A10 -B2 "jpa\|hibernate\|sql" || true

Repository: swyp-4th-team7/server

Length of output: 9484


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Full application.yml content ==="
cat src/main/resources/application.yml

echo ""
echo "=== Full build.gradle to check for DB setup ==="
cat build.gradle | grep -A20 -B5 "jpa\|hibernate\|database\|h2\|mysql" || true

echo ""
echo "=== Checking Habit.java for `@GeneratedValue` and other schema hints ==="
head -60 src/main/java/com/swyp/server/domain/habit/entity/Habit.java

Repository: swyp-4th-team7/server

Length of output: 4234


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Complete Habit.java to see full builder ==="
cat src/main/java/com/swyp/server/domain/habit/entity/Habit.java

Repository: swyp-4th-team7/server

Length of output: 2571


Verify deployment strategy for databases with existing habit rows.

The builder correctly initializes failCount to 0 for new entities. However, Hibernate's ddl-auto: update cannot safely add the NOT NULL fail_count column to an existing habits table with rows—it requires an explicit backfill strategy. For fresh database instances or new project deployments, this works fine. If deploying to a database that already contains habit records, you must either:

  • Use manual SQL to add the column with a default value and backfill existing rows, or
  • Configure a migration tool (Flyway/Liquibase) to handle schema changes safely.

Confirm your deployment target supports this constraint.

🤖 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 49
- 50, The new non-nullable field Habit.failCount (annotated `@Column`(nullable =
false)) will fail on existing databases when Hibernate ddl-auto=update tries to
add the column without backfilling; confirm your deployment target is a fresh DB
or else create a migration/backfill: either run a manual ALTER TABLE to add
fail_count with a DEFAULT 0 and update existing rows, or add a Flyway/Liquibase
migration that alters the habits table and sets fail_count = 0 for existing
records (or temporarily make the column nullable/defaulted then flip to NOT
NULL), and ensure the Builder that initializes failCount to 0 remains for new
entities.

Comment on lines +13 to +14
ALL("전체"),
FAIL("실패");

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

Don't expose FAIL through the reward-tab filter enum.

RewardStatus is bound directly from GET /api/v1/habits/rewards?status=..., and HabitService.determineSearchStatuses() passes the selected status through. Adding FAIL here makes failed habits requestable from the reward tab, which conflicts with the requirement that failed habits must not be shown there.

🤖 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/RewardStatus.java` around
lines 13 - 14, Remove the FAIL enum constant from RewardStatus so the reward-tab
filter cannot bind to a "FAIL" value; update any code that relied on
RewardStatus.FAIL (notably HabitService.determineSearchStatuses()) to use a
different status or handle failed-habit logic separately (e.g., use a separate
enum or explicit check) so failed habits are never returned for the reward tab.
Ensure RewardStatus only contains values intended for the rewards filter (e.g.,
ALL and any other allowed states) and adjust
HabitService.determineSearchStatuses() to map incoming filter params to the
appropriate allowed RewardStatus values or alternative handling.

Comment on lines +27 to +29
@EntityGraph(attributePaths = "user")
@Query("SELECT h FROM Habit h " + "WHERE h.user.id = :userId " + "AND h.status = 'FAIL'")
List<Habit> findAllFailedHabitsByUserId(@Param("userId") Long userId);

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -i 'HabitService.java' src/main/java -x rg -n -C3 'getFailedHabits|findAllFailedHabitsByUserId|expiredAt|retry' {}

Repository: swyp-4th-team7/server

Length of output: 580


🏁 Script executed:

fd -i 'FailedHabitListResponse.java' src/main/java -x cat {} | head -100

Repository: swyp-4th-team7/server

Length of output: 439


🏁 Script executed:

fd -i 'Habit.java' src/main/java -type f -path '*/domain/habit/entity/*' -x cat {} | head -150

Repository: swyp-4th-team7/server

Length of output: 235


🏁 Script executed:

rg -n 'expiredAt|retry.*period|RETRY_PERIOD' src/main/java --max-count=20

Repository: swyp-4th-team7/server

Length of output: 760


🏁 Script executed:

rg -n -A 10 'public void retryFailedHabit' src/main/java

Repository: swyp-4th-team7/server

Length of output: 1349


Limit failed habit retrieval to still-retryable items within the expiration window.

Currently, findAllFailedHabitsByUserId() returns all FAIL status habits without an expiredAt boundary. The service layer applies no downstream filter, so expired failures leak into the API response. Since the feature targets retryable habits within the configured period, add an expiredAt >= :now predicate to align with the existing pattern used elsewhere in HabitRepository (lines 47, 60):

Suggested fix
-    `@Query`("SELECT h FROM Habit h " + "WHERE h.user.id = :userId " + "AND h.status = 'FAIL'")
-    List<Habit> findAllFailedHabitsByUserId(`@Param`("userId") Long userId);
+    `@Query`(
+            "SELECT h FROM Habit h "
+                    + "WHERE h.user.id = :userId "
+                    + "AND h.status = 'FAIL' "
+                    + "AND h.expiredAt >= :now "
+                    + "ORDER BY h.id DESC")
+    List<Habit> findAllFailedHabitsByUserId(
+            `@Param`("userId") Long userId, `@Param`("now") LocalDateTime now);

Update the service call in HabitService.getFailedHabits() to pass the current time parameter.

📝 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.

Suggested change
@EntityGraph(attributePaths = "user")
@Query("SELECT h FROM Habit h " + "WHERE h.user.id = :userId " + "AND h.status = 'FAIL'")
List<Habit> findAllFailedHabitsByUserId(@Param("userId") Long userId);
`@EntityGraph`(attributePaths = "user")
`@Query`(
"SELECT h FROM Habit h "
"WHERE h.user.id = :userId "
"AND h.status = 'FAIL' "
"AND h.expiredAt >= :now "
"ORDER BY h.id DESC")
List<Habit> findAllFailedHabitsByUserId(
`@Param`("userId") Long userId, `@Param`("now") LocalDateTime now);
🤖 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/repository/HabitRepository.java`
around lines 27 - 29, The query method findAllFailedHabitsByUserId currently
returns all habits with status 'FAIL' regardless of expiration; update its JPQL
to add an "AND h.expiredAt >= :now" predicate (matching the pattern used in
other repository queries) and add a new `@Param`("now") parameter, then update the
service method HabitService.getFailedHabits() to pass the current
Instant/LocalDateTime as the now parameter when calling
findAllFailedHabitsByUserId so only still-retryable failures within the
expiration window are returned.

Comment on lines +71 to +77
@Modifying(clearAutomatically = true)
@Query(
"UPDATE Habit h SET h.status = 'FAIL' "
+ " WHERE h.failCount > 1 "
+ " AND h.status IN ('REWARD_CHECKING', 'IN_PROGRESS')"
+ " AND h.duration NOT IN ('THREE_DAYS', 'SEVEN_DAYS')")
void updateCumulativeFailureHabits();

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -i 'HabitScheduler.java' src/main/java -x rg -n -C4 'checkExpiredHabits|updateImmediateFailureHabits|updateCumulativeFailureHabits|updateHabitFailCount|resetFailCount' {}

Repository: swyp-4th-team7/server

Length of output: 775


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# First, find and examine the Habit entity
fd -i 'Habit.java' src/main/java -type f | head -5

Repository: swyp-4th-team7/server

Length of output: 235


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for isCompleted field usage in the codebase
rg 'isCompleted' src/main/java --type java -B2 -A2 | head -80

Repository: swyp-4th-team7/server

Length of output: 6883


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look at the updateHabitFailCount method implementation
rg -n 'updateHabitFailCount' src/main/java --type java -A10

Repository: swyp-4th-team7/server

Length of output: 2655


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the Habit entity file and get line count
fd -i 'Habit.java' src/main/java -path '*/entity/*' -type f

Repository: swyp-4th-team7/server

Length of output: 233


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get the full updateHabitFailCount method
rg -n 'updateHabitFailCount' src/main/java -B2 -A15

Repository: swyp-4th-team7/server

Length of output: 4334


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get the complete updateCumulativeFailureHabits query
rg -n 'updateCumulativeFailureHabits' src/main/java -B2 -A8

Repository: swyp-4th-team7/server

Length of output: 2691


Add isCompleted = false check to prevent failing habits that were completed today.

The query marks a habit FAIL whenever failCount > 1, but doesn't verify whether the habit was actually incomplete today. This allows a habit with accumulated failures from previous days to be marked FAIL even if isCompleted = true in the current cycle. Since updateHabitFailCount() only increments the counter when isCompleted = false, the cumulative check should do the same.

Required fix
     `@Query`(
             "UPDATE Habit h SET h.status = 'FAIL' "
-                    + " WHERE h.failCount > 1 "
+                    + " WHERE h.isCompleted = false "
+                    + " AND h.failCount > 1 "
                     + " AND h.status IN ('REWARD_CHECKING', 'IN_PROGRESS')"
                     + " AND h.duration NOT IN ('THREE_DAYS', 'SEVEN_DAYS')")
🤖 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/repository/HabitRepository.java`
around lines 71 - 77, The JPQL in updateCumulativeFailureHabits() currently sets
status='FAIL' for habits with failCount>1 without checking today's completion;
modify the query to include a check that h.isCompleted = false so only habits
not completed in the current cycle are failed (keep the existing conditions on
h.failCount, h.status, and h.duration). Locate the method
updateCumulativeFailureHabits() in HabitRepository and add the isCompleted
condition to the WHERE clause to mirror updateHabitFailCount() behavior.

Comment on lines +33 to +39
log.info("실패 습관 확인 스케줄러 시작");
habitRepository.updateHabitFailCount();
habitRepository.updateImmediateFailureHabits();
habitRepository.updateCumulativeFailureHabits();

log.info("습관 실패 횟수 초기화 스케줄러 시작");
habitRepository.resetFailCount(now);

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

Apply the 10-day reset before escalating to FAIL.

For 14–99 day habits, the new policy says the failure count resets every 10 days. In the current order, a habit with failCount = 1 that misses on day 10 is incremented to 2 and marked FAIL before resetFailCount(now) runs, and then the reset no longer applies because the status is already FAIL. Reorder these calls or fold the reset condition into the failure queries.

🤖 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/HabitScheduler.java`
around lines 33 - 39, The current order applies failure escalations before the
10-day reset, causing habits to be marked FAIL prematurely; in HabitScheduler
change the sequence so habitRepository.resetFailCount(now) runs before
habitRepository.updateHabitFailCount(), updateImmediateFailureHabits(), and
updateCumulativeFailureHabits() (or alternatively incorporate the 10-day reset
condition into the failure update queries), ensuring updateHabitFailCount(),
updateImmediateFailureHabits(), and updateCumulativeFailureHabits() operate on
already-reset fail counts.

Comment on lines +229 to +236
@Transactional
public void retryFailedHabit(Long userId, Long habitId) {
Habit habit =
habitRepository
.findByIdAndUserId(habitId, userId)
.orElseThrow(() -> new CustomException(ErrorCode.HABIT_NOT_FOUND));
habit.updateRewardStatus(RewardStatus.IN_PROGRESS);
}

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 | 🔴 Critical

Retry needs state validation and a full state reset.

This currently rewrites any owned habit to IN_PROGRESS. That lets non-FAIL habits be retried, ignores the "within configured period" constraint, and leaves 14–99 day habits with their previous failCount, so they can fall back to FAIL again on the next scheduler run. It also breaks the child flow from the issue: child habits should go back to REWARD_CHECKING so the child sees 진행중 while the parent sees 보상 확인중.

🤖 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 229 - 236, The retryFailedHabit method must only operate on habits that
are actually in FAIL and within the configured retry window, and it must perform
a full state reset (clear failCount and relevant timestamps) and set the correct
post-retry status depending on parent/child semantics. Update retryFailedHabit
to: 1) verify habit.getRewardStatus() == RewardStatus.FAIL and check a
retry-window predicate (e.g., habit.isWithinRetryPeriod() or compare
habit.getFailedAt()/habit.getConfiguredPeriod()), throwing
CustomException(ErrorCode.INVALID_HABIT_STATE) if not; 2) reset state fields
like habit.setFailCount(0) and clear/refresh any failure timestamps; and 3) set
reward status appropriately — for child habits use RewardStatus.REWARD_CHECKING
(so child shows 진행중 while parent shows 보상 확인중), otherwise set
RewardStatus.IN_PROGRESS for owning/parent habits; keep these updates inside the
existing `@Transactional` retryFailedHabit method and use the existing Habit
methods (or add small helpers on Habit) to perform the resets.

@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.

♻️ Duplicate comments (2)
src/main/java/com/swyp/server/domain/habit/repository/HabitRepository.java (2)

29-31: ⚠️ Potential issue | 🟠 Major

Filter failed habits to the active retry window.

Line 30 still returns every FAIL habit for the user, so expired failures leak into the failed-habits API as if they were still retryable. Please add the expiredAt boundary here and pass now from HabitService.getFailedHabits(...).

Suggested fix
 `@EntityGraph`(attributePaths = "user")
-@Query("SELECT h FROM Habit h " + "WHERE h.user.id = :userId " + "AND h.status = 'FAIL'")
-List<Habit> findAllFailedHabitsByUserId(`@Param`("userId") Long userId);
+@Query(
+        "SELECT h FROM Habit h "
+                + "WHERE h.user.id = :userId "
+                + "AND h.status = 'FAIL' "
+                + "AND h.expiredAt >= :now "
+                + "ORDER BY h.id DESC")
+List<Habit> findAllFailedHabitsByUserId(
+        `@Param`("userId") Long userId, `@Param`("now") LocalDateTime now);
🤖 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/repository/HabitRepository.java`
around lines 29 - 31, The repository query findAllFailedHabitsByUserId currently
returns all FAIL habits; restrict it to the active retry window by adding an
expiredAt boundary and a now parameter: update the JPQL in
findAllFailedHabitsByUserId (method name) to include "AND h.expiredAt > :now"
(or use >= if your semantics require inclusivity) and add a corresponding
`@Param`("now") Instant now parameter so HabitService.getFailedHabits(...) can
pass the current time.

73-79: ⚠️ Potential issue | 🟠 Major

Don't fail habits that were completed in the current cycle.

Line 76 only checks failCount > 1. A habit that already accumulated failures can still be marked FAIL here even when isCompleted = true today. This should mirror the incomplete-only guard already used in updateHabitFailCount().

Suggested fix
 `@Query`(
         "UPDATE Habit h SET h.status = 'FAIL' "
-                + " WHERE h.failCount > 1 "
+                + " WHERE h.isCompleted = false "
+                + " AND h.failCount > 1 "
                 + " AND h.status IN ('REWARD_CHECKING', 'IN_PROGRESS')"
                 + " AND h.duration NOT IN ('THREE_DAYS', 'SEVEN_DAYS')")
🤖 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/repository/HabitRepository.java`
around lines 73 - 79, The query in updateCumulativeFailureHabits() is
incorrectly marking habits as FAIL even if they are completed today; add the
same incomplete guard used in updateHabitFailCount() so the UPDATE only targets
habits where isCompleted = false (or the equivalent boolean field), i.e.,
include "AND h.isCompleted = false" (or the matching property name) alongside
the existing predicates to prevent failing habits completed in the current
cycle.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/main/java/com/swyp/server/domain/habit/repository/HabitRepository.java`:
- Around line 29-31: The repository query findAllFailedHabitsByUserId currently
returns all FAIL habits; restrict it to the active retry window by adding an
expiredAt boundary and a now parameter: update the JPQL in
findAllFailedHabitsByUserId (method name) to include "AND h.expiredAt > :now"
(or use >= if your semantics require inclusivity) and add a corresponding
`@Param`("now") Instant now parameter so HabitService.getFailedHabits(...) can
pass the current time.
- Around line 73-79: The query in updateCumulativeFailureHabits() is incorrectly
marking habits as FAIL even if they are completed today; add the same incomplete
guard used in updateHabitFailCount() so the UPDATE only targets habits where
isCompleted = false (or the equivalent boolean field), i.e., include "AND
h.isCompleted = false" (or the matching property name) alongside the existing
predicates to prevent failing habits completed in the current cycle.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a840063d-1b21-4f4e-b355-8a11e8814309

📥 Commits

Reviewing files that changed from the base of the PR and between add6098 and 6978695.

📒 Files selected for processing (3)
  • 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
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/swyp/server/domain/habit/entity/Habit.java

+ " WHERE h.failCount > 1 "
+ " AND h.status IN ('REWARD_CHECKING', 'IN_PROGRESS')"
+ " AND h.duration NOT IN ('THREE_DAYS', 'SEVEN_DAYS')")
void updateCumulativeFailureHabits();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P1: updateCumulativeFailureHabits 쿼리에 isCompleted = false 조건 추가가 필요할 것 같습니다. 오늘 완료한 습관도 failCount > 1이면 FAIL이 될 수 있습니다!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

윤미님 좋은 의견 감사합니다!
하지만 해당 로직은 금일 완료 여부와 상관 없이 누적된 습관 실패 횟수를 기반으로 동작하는 것을 의도로 설계를 했기 때문에 수정하지 않아도 괜찮을 거라 생각합니다.
혹시 제가 오해한 부분이 있으면 코멘트 남겨주세요..!!

habitRepository.updateCumulativeFailureHabits();

log.info("습관 실패 횟수 초기화 스케줄러 시작");
habitRepository.resetFailCount(now);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P1: resetFailCount가 updateCumulativeFailureHabits 이후에 실행되고 있어서 10일째 되는 날 failCount = 1인 습관이 2로 증가 후 FAIL 처리되고 나서 초기화가 되지 않는 문제가 있을 것 같습니다. resetFailCount를 먼저 실행하는 게 맞지 않을까 싶어서 의견 남깁니다!!

@guzzang guzzang Apr 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

윤미님 저도 해당 부분이 헷갈려서 저희 서비스 정책을 확인해보니, 10일에 2번 이상 실패하면 FAIL 처리되도록 정책이 설정되어 있어서 failCount를 먼저 증가시키고 초기화 시키는 순서가 맞다고 생각이 들어 위와 같이 로직을 작성하게 되었습니다.
혹시 다른 의도로 의견 남겨주신거면 코멘트 부탁드립니다! 의견 주셔서 감사해요 :)

COMPLETE("완료"),
ALL("전체");
ALL("전체"),
FAIL("실패");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P2: FAIL이 RewardStatus에 추가되면 보상 탭에서 ?status=FAIL로 조회가 가능해집니다. determineSearchStatuses에서 FAIL을 필터링해주는 처리가 필요할 것 같습니다!

private LocalDateTime expiredAt;

@Column(nullable = false)
private int failCount;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P2: 기존 DB에 habits 데이터가 있는 경우 NOT NULL 컬럼 추가 시 오류가 날 수 있습니다. 배포 전 RDS에서 ALTER TABLE habits ADD COLUMN fail_count INT NOT NULL DEFAULT 0; 실행이 필요합니다!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

해당 쿼리 반영하겠습니다! 감사합니다 :)

@yunmi-dev yunmi-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

고생하셨습니다!

@guzzang
guzzang merged commit 6bf81be into develop Apr 18, 2026
1 check passed
@guzzang
guzzang deleted the feat/#110-retry-habit branch April 18, 2026 04:30
@guzzang guzzang changed the title Feat/#110 retry habit Feat/#110 습관 재도전 기능 구현 Apr 19, 2026
@guzzang guzzang changed the title Feat/#110 습관 재도전 기능 구현 [Feat/#110] 습관 재도전 기능 구현 Apr 19, 2026
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