Skip to content

Commit 2effe3f

Browse files
authored
[Fix/#90] 습관 완료 이력 unique key를 habit_id+날짜로 변경 - 중복 저장 오류 수정 위함 (#91)
## 📌 관련 이슈 - close #90 ## ✨ 변경 사항 - HabitDailyCompletion unique key를 user_id+날짜 → habit_id+날짜로 변경 - HabitService.updateHabit 완료 이력 저장/삭제 시 habit_id 기준으로 변경 - HabitDailyCompletionRepository countCompletionsByUserIds를 COUNT(DISTINCT completion_date)로 수정 - GrowthService.getHabitGrowth distinct 날짜 수 기반으로 별 계산 ## 📚 리뷰어 참고 사항 - 기존 habit_daily_completions 테이블 스키마 변경 필요 - 민구님 기존 습관 로직(updateHabit, resetDailyHabits) 변경 없음 ## ✅ 체크리스트 - [x] 브랜치 전략(git flow)을 따랐나요? - [x] 로컬에서 빌드 및 실행이 정상적으로 되나요? - [x] 불필요한 주석이나 더미 코드는 제거했나요? - [x] 컨벤션(커밋 메시지, 코드 스타일)을 지켰나요? - [x] spotlessApply 실행했나요? <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Deduplicated daily completion counts by date so progress and stars reflect distinct days. * Tied completion records to specific habits, ensuring correct creation and deletion of daily completions. * **Refactor** * Completion updates now depend on actual state changes (completed ↔ not completed) to avoid redundant writes/deletes. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 97404f4 commit 2effe3f

4 files changed

Lines changed: 31 additions & 25 deletions

File tree

src/main/java/com/swyp/server/domain/growth/service/GrowthService.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,16 @@ public GrowthHabitResponse getHabitGrowth(Long userId) {
5050
LocalDate startDate = DateUtils.getWeekStart(today);
5151
LocalDate endDate = DateUtils.getWeekEnd(today);
5252

53-
int completedDays =
53+
// habit_id별 이력이 있으므로 distinct 날짜 수로 계산
54+
long completedDays =
5455
habitDailyCompletionRepository
5556
.findAllByUserIdAndCompletionDateBetween(userId, startDate, endDate)
56-
.size();
57-
int starCount = calculateHabitStarCount(completedDays);
57+
.stream()
58+
.map(c -> c.getCompletionDate())
59+
.distinct()
60+
.count();
61+
62+
int starCount = calculateHabitStarCount((int) completedDays);
5863
String weekRange = formatWeekRange(startDate, endDate);
5964

6065
return new GrowthHabitResponse(starCount, weekRange);

src/main/java/com/swyp/server/domain/habit/entity/HabitDailyCompletion.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
name = "habit_daily_completions",
2323
uniqueConstraints = {
2424
@UniqueConstraint(
25-
name = "uk_habit_daily_completion_user_date",
26-
columnNames = {"user_id", "completion_date"})
25+
name = "uk_habit_daily_completion_habit_date",
26+
columnNames = {"habit_id", "completion_date"})
2727
})
2828
@Getter
2929
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@@ -37,12 +37,17 @@ public class HabitDailyCompletion {
3737
@JoinColumn(name = "user_id", nullable = false)
3838
private User user;
3939

40+
@ManyToOne(fetch = FetchType.LAZY, optional = false)
41+
@JoinColumn(name = "habit_id", nullable = false)
42+
private Habit habit;
43+
4044
@Column(name = "completion_date", nullable = false)
4145
private LocalDate completionDate;
4246

4347
@Builder
44-
public HabitDailyCompletion(User user, LocalDate completionDate) {
45-
this.user = user;
48+
public HabitDailyCompletion(Habit habit, LocalDate completionDate) {
49+
this.user = habit.getUser();
50+
this.habit = habit;
4651
this.completionDate = completionDate;
4752
}
4853
}

src/main/java/com/swyp/server/domain/habit/repository/HabitDailyCompletionRepository.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ List<HabitDailyCompletion> findAllByUserIdAndCompletionDateBetween(
1414
Long userId, LocalDate startDate, LocalDate endDate);
1515

1616
@Query(
17-
"SELECT h.user.id, COUNT(h.id) FROM HabitDailyCompletion h "
17+
"SELECT h.user.id, COUNT(DISTINCT h.completionDate) FROM HabitDailyCompletion h "
1818
+ "WHERE h.user.id IN :userIds "
1919
+ "AND h.completionDate BETWEEN :startDate AND :endDate "
2020
+ "GROUP BY h.user.id")
@@ -29,5 +29,5 @@ List<Object[]> countCompletionsByUserIds(
2929
nativeQuery = true)
3030
void hardDeleteAllByUserId(@Param("userId") Long userId);
3131

32-
void deleteByUserIdAndCompletionDate(Long userId, LocalDate completionDate);
32+
void deleteByHabitIdAndCompletionDate(Long habitId, LocalDate completionDate);
3333
}

src/main/java/com/swyp/server/domain/habit/service/HabitService.java

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -173,28 +173,24 @@ public void updateHabit(Long userId, Long habitId, HabitUpdateRequest request) {
173173
.findByIdAndUserId(habitId, userId)
174174
.orElseThrow(() -> new CustomException(ErrorCode.HABIT_NOT_FOUND));
175175

176+
boolean wasCompleted = habit.isCompleted();
177+
boolean nowCompleted = request.isCompleted();
178+
LocalDate today = LocalDate.now(ZoneId.of("Asia/Seoul"));
179+
176180
habit.updateTitle(request.title());
177181
habit.updateDuration(request.duration());
178182
habit.updateReward(reward);
179183

180-
if (request.isCompleted()) {
181-
habit.complete();
182-
LocalDate today = LocalDate.now(ZoneId.of("Asia/Seoul"));
183-
// 오늘 이미 기록된 경우에 중복 저장 방지
184-
try {
184+
if (wasCompleted != nowCompleted) {
185+
if (nowCompleted) {
186+
habit.complete();
185187
habitDailyCompletionRepository.save(
186-
HabitDailyCompletion.builder()
187-
.user(habit.getUser())
188-
.completionDate(today)
189-
.build());
190-
} catch (org.springframework.dao.DataIntegrityViolationException ignored) {
191-
// 동시 요청으로 이미 오늘 완료 이력이 있으면 무시
188+
HabitDailyCompletion.builder().habit(habit).completionDate(today).build());
189+
} else {
190+
habit.incomplete();
191+
habitDailyCompletionRepository.deleteByHabitIdAndCompletionDate(
192+
habit.getId(), today);
192193
}
193-
} else {
194-
habit.incomplete();
195-
// 오늘 완료 이력 삭제
196-
LocalDate today = LocalDate.now(ZoneId.of("Asia/Seoul"));
197-
habitDailyCompletionRepository.deleteByUserIdAndCompletionDate(userId, today);
198194
}
199195
}
200196

0 commit comments

Comments
 (0)