Skip to content

Commit 95a6984

Browse files
authored
Merge pull request #306 from SWM16-ASAP/develop
Release: v3.1.5
2 parents f34720d + 3eebb8f commit 95a6984

3 files changed

Lines changed: 100 additions & 69 deletions

File tree

‎src/main/java/com/linglevel/api/streak/repository/DailyCompletionRepository.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,10 @@ public interface DailyCompletionRepository extends MongoRepository<DailyCompleti
3434
* 특정 사용자의 모든 DailyCompletion을 날짜 오름차순으로 조회
3535
*/
3636
List<DailyCompletion> findByUserIdOrderByCompletionDateAsc(String userId);
37+
38+
/**
39+
* 특정 날짜 이상의 DailyCompletion을 날짜 오름차순으로 조회 (스트릭 복구용)
40+
*/
41+
@Query("{ 'userId': ?0, 'completionDate': { $gte: ?1 } }")
42+
List<DailyCompletion> findByUserIdAndCompletionDateGreaterThanEqualOrderByCompletionDateAsc(String userId, LocalDate startDate);
3743
}

‎src/main/java/com/linglevel/api/streak/service/StreakService.java‎

Lines changed: 82 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -932,37 +932,25 @@ public void recoverStreak(String userId, LocalDate startDate, LocalDate endDate)
932932
endDate = today;
933933
}
934934

935-
// 1. 시작 전날 확인 (streakCount 기준 계산)
936-
LocalDate dayBeforeStart = startDate.minusDays(1);
937-
int baseStreakCount = dailyCompletionRepository
938-
.findByUserIdAndCompletionDate(userId, dayBeforeStart)
939-
.map(DailyCompletion::getStreakCount)
940-
.orElse(0);
935+
// 복구 전 프리즈 개수 저장
936+
UserStudyReport beforeReport = userStudyReportRepository.findByUserId(userId).orElse(null);
937+
int freezesBeforeRecovery = beforeReport != null && beforeReport.getAvailableFreezes() != null
938+
? beforeReport.getAvailableFreezes() : 0;
941939

942-
// 2. 복구 처리 및 프리즈 보상 수집
940+
// 1. 복구 처리 및 프리즈 보상 수집
943941
List<DailyCompletion> completionsToSave = new ArrayList<>();
944942
List<FreezeTransaction> freezeTransactions = new ArrayList<>();
945-
int currentStreakCount = baseStreakCount;
946943
int earnedFreezes = 0;
947944

948-
// 복구 범위 처리
945+
// 복구 범위 처리 (streakCount는 나중에 재계산)
949946
for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {
950947
Optional<DailyCompletion> existingOpt = dailyCompletionRepository
951948
.findByUserIdAndCompletionDate(userId, date);
952949

953950
if (existingOpt.isPresent()) {
954951
DailyCompletion existing = existingOpt.get();
955952

956-
if (existing.getStreakStatus() == StreakStatus.COMPLETED) {
957-
// 이미 완료됨 - 그대로 유지
958-
currentStreakCount++;
959-
} else if (existing.getStreakStatus() == StreakStatus.FREEZE_USED) {
960-
// FREEZE_USED → COMPLETED로 변경 + 프리즈 보상
961-
existing.setStreakStatus(StreakStatus.COMPLETED);
962-
currentStreakCount++;
963-
existing.setStreakCount(currentStreakCount);
964-
completionsToSave.add(existing);
965-
953+
if (existing.getStreakStatus() == StreakStatus.FREEZE_USED) {
966954
// 프리즈 보상
967955
FreezeTransaction rewardTx = FreezeTransaction.builder()
968956
.userId(userId)
@@ -975,61 +963,57 @@ public void recoverStreak(String userId, LocalDate startDate, LocalDate endDate)
975963

976964
log.info("Recovered FREEZE_USED to COMPLETED for user {} on {}. Rewarded 1 freeze.",
977965
userId, date);
978-
} else {
979-
// MISSED 또는 기타 → COMPLETED로 변경
980-
existing.setStreakStatus(StreakStatus.COMPLETED);
981-
currentStreakCount++;
982-
existing.setStreakCount(currentStreakCount);
983-
completionsToSave.add(existing);
984966
}
967+
968+
// streakCount는 나중에 재계산할 것이므로 null로 설정
969+
existing.setStreakStatus(StreakStatus.COMPLETED);
970+
existing.setStreakCount(null);
971+
completionsToSave.add(existing);
972+
985973
} else {
986974
// 레코드 없음 (MISSED) → 새로 생성
987-
currentStreakCount++;
988975
DailyCompletion newCompletion = DailyCompletion.builder()
989976
.userId(userId)
990977
.completionDate(date)
991978
.streakStatus(StreakStatus.COMPLETED)
992-
.streakCount(currentStreakCount)
979+
.streakCount(null) // 나중에 재계산
993980
.firstCompletionCount(0)
994981
.totalCompletionCount(0)
995982
.completedContents(new ArrayList<>())
996983
.createdAt(Instant.now())
997984
.build();
998985
completionsToSave.add(newCompletion);
999986

1000-
log.info("Created new COMPLETED record for user {} on {}. StreakCount: {}",
1001-
userId, date, currentStreakCount);
987+
log.info("Created new COMPLETED record for user {} on {}.", userId, date);
1002988
}
1003989
}
1004990

1005-
// 3. 복구 범위 이후 날짜들 처리 (프리즈 자동 사용)
1006-
LocalDate yesterday = today.minusDays(1);
991+
// 2. 복구 범위 이후 날짜들 처리 (프리즈 자동 사용)
1007992
LocalDate currentDate = endDate.plusDays(1);
1008993
int availableFreezes = earnedFreezes;
1009994

1010-
while (!currentDate.isAfter(yesterday)) {
995+
while (currentDate.isBefore(today)) { // 오늘은 제외
1011996
Optional<DailyCompletion> existingOpt = dailyCompletionRepository
1012997
.findByUserIdAndCompletionDate(userId, currentDate);
1013998

1014999
if (existingOpt.isPresent()) {
10151000
DailyCompletion existing = existingOpt.get();
10161001

10171002
if (existing.getStreakStatus() == StreakStatus.COMPLETED) {
1018-
// 완료됨 - 스트릭 계속 증가
1019-
currentStreakCount++;
1020-
existing.setStreakCount(currentStreakCount);
1003+
// 이미 완료됨 - 그대로 유지 (streakCount는 재계산)
1004+
existing.setStreakCount(null);
10211005
completionsToSave.add(existing);
10221006
currentDate = currentDate.plusDays(1);
10231007
} else if (existing.getStreakStatus() == StreakStatus.FREEZE_USED) {
1024-
// 이미 프리즈 사용됨 - 스트릭 유지
1025-
existing.setStreakCount(currentStreakCount);
1008+
// 이미 프리즈 사용됨 - 유지
1009+
existing.setStreakCount(null);
10261010
completionsToSave.add(existing);
10271011
currentDate = currentDate.plusDays(1);
10281012
} else {
10291013
// MISSED - 프리즈로 커버 시도
10301014
if (availableFreezes > 0) {
10311015
existing.setStreakStatus(StreakStatus.FREEZE_USED);
1032-
existing.setStreakCount(currentStreakCount);
1016+
existing.setStreakCount(null);
10331017
completionsToSave.add(existing);
10341018

10351019
// 프리즈 사용
@@ -1060,7 +1044,7 @@ public void recoverStreak(String userId, LocalDate startDate, LocalDate endDate)
10601044
.userId(userId)
10611045
.completionDate(currentDate)
10621046
.streakStatus(StreakStatus.FREEZE_USED)
1063-
.streakCount(currentStreakCount)
1047+
.streakCount(null) // 재계산 예정
10641048
.firstCompletionCount(0)
10651049
.totalCompletionCount(0)
10661050
.completedContents(new ArrayList<>())
@@ -1073,8 +1057,6 @@ public void recoverStreak(String userId, LocalDate startDate, LocalDate endDate)
10731057
.userId(userId)
10741058
.amount(-1)
10751059
.description("Auto-consumed for recovery on " + currentDate)
1076-
1077-
10781060
.createdAt(Instant.now())
10791061
.build();
10801062
freezeTransactions.add(usageTx);
@@ -1093,25 +1075,7 @@ public void recoverStreak(String userId, LocalDate startDate, LocalDate endDate)
10931075
}
10941076
}
10951077

1096-
// 4. 오늘(today) 처리 - 프리즈 사용은 안 하지만, 학습했다면 streakCount 업데이트
1097-
Optional<DailyCompletion> todayCompletionOpt = dailyCompletionRepository
1098-
.findByUserIdAndCompletionDate(userId, today);
1099-
1100-
if (todayCompletionOpt.isPresent()) {
1101-
DailyCompletion todayCompletion = todayCompletionOpt.get();
1102-
1103-
if (todayCompletion.getStreakStatus() == StreakStatus.COMPLETED) {
1104-
// 오늘 학습함 - streakCount 업데이트
1105-
currentStreakCount++;
1106-
todayCompletion.setStreakCount(currentStreakCount);
1107-
completionsToSave.add(todayCompletion);
1108-
1109-
log.info("Updated today's completion for user {} with streakCount: {}", userId, currentStreakCount);
1110-
}
1111-
// MISSED나 다른 상태면 배치에서 처리하므로 여기서는 스킵
1112-
}
1113-
1114-
// 5. 데이터 저장
1078+
// 3. 데이터 저장
11151079
if (!completionsToSave.isEmpty()) {
11161080
dailyCompletionRepository.saveAll(completionsToSave);
11171081
log.info("Saved {} DailyCompletion records for user {}", completionsToSave.size(), userId);
@@ -1122,20 +1086,69 @@ public void recoverStreak(String userId, LocalDate startDate, LocalDate endDate)
11221086
log.info("Saved {} FreezeTransaction records for user {}", freezeTransactions.size(), userId);
11231087
}
11241088

1089+
// 4. 전체 streakCount 재계산 (startDate 기준)
1090+
recalculateAllStreakCounts(userId, startDate);
1091+
11251092
// 5. UserStudyReport 재계산 및 프리즈 반영
11261093
UserStudyReport finalReport = recalculateUserStudyReport(userId);
11271094

1128-
// 프리즈 개수 업데이트
1129-
// availableFreezes는 복구 과정에서 남은 프리즈 개수
1130-
// 최종 프리즈 = 기존 보유 + 남은 프리즈 (최대 MAX_FREEZE_COUNT)
1131-
int currentFreezes = finalReport.getAvailableFreezes() != null ? finalReport.getAvailableFreezes() : 0;
1132-
int usedFreezes = earnedFreezes - availableFreezes;
1133-
finalReport.setAvailableFreezes(Math.max(0, Math.min(MAX_FREEZE_COUNT, currentFreezes + availableFreezes)));
1095+
// 최종 프리즈 = 복구 전 + 획득 - 사용 (최대 MAX_FREEZE_COUNT)
1096+
int finalFreezes = Math.min(MAX_FREEZE_COUNT, freezesBeforeRecovery + availableFreezes);
1097+
finalReport.setAvailableFreezes(finalFreezes);
11341098
userStudyReportRepository.save(finalReport);
11351099

1100+
int usedFreezes = earnedFreezes - availableFreezes;
11361101
log.info("Streak recovery completed for user {} from {} to {}. Earned {} freezes, used {} freezes. Final freezes: {}",
1137-
userId, startDate, endDate, earnedFreezes, usedFreezes, finalReport.getAvailableFreezes());
1102+
userId, startDate, endDate, earnedFreezes, usedFreezes, finalFreezes);
11381103
}
1139-
}
11401104

1141-
1105+
@Transactional
1106+
public void recalculateAllStreakCounts(String userId, LocalDate recoveryStartDate) {
1107+
// 복구 시작일 전날의 streakCount를 기준값으로 가져오기
1108+
LocalDate dayBeforeStart = recoveryStartDate.minusDays(1);
1109+
int baseStreakCount = dailyCompletionRepository
1110+
.findByUserIdAndCompletionDate(userId, dayBeforeStart)
1111+
.map(DailyCompletion::getStreakCount)
1112+
.orElse(0);
1113+
1114+
log.info("Starting recalculation from {} with base streakCount: {}", recoveryStartDate, baseStreakCount);
1115+
1116+
// 복구 시작일부터 모든 DailyCompletion 가져오기
1117+
List<DailyCompletion> completionsToRecalculate = dailyCompletionRepository
1118+
.findByUserIdAndCompletionDateGreaterThanEqualOrderByCompletionDateAsc(userId, recoveryStartDate);
1119+
1120+
if (completionsToRecalculate.isEmpty()) {
1121+
log.info("No completions found for user {} from {}. Skipping streakCount recalculation.",
1122+
userId, recoveryStartDate);
1123+
return;
1124+
}
1125+
1126+
int streakCount = baseStreakCount;
1127+
LocalDate previousDate = dayBeforeStart;
1128+
List<DailyCompletion> toUpdate = new ArrayList<>();
1129+
1130+
for (DailyCompletion completion : completionsToRecalculate) {
1131+
LocalDate currentDate = completion.getCompletionDate();
1132+
StreakStatus status = completion.getStreakStatus();
1133+
1134+
// 날짜가 연속적이지 않으면 스트릭 리셋
1135+
if (ChronoUnit.DAYS.between(previousDate, currentDate) > 1 || status == StreakStatus.MISSED) {
1136+
break;
1137+
}
1138+
1139+
if (status == StreakStatus.COMPLETED) {
1140+
streakCount++;
1141+
}
1142+
1143+
completion.setStreakCount(streakCount);
1144+
toUpdate.add(completion);
1145+
previousDate = currentDate;
1146+
}
1147+
1148+
if (!toUpdate.isEmpty()) {
1149+
dailyCompletionRepository.saveAll(toUpdate);
1150+
log.info("Recalculated {} streakCounts for user {} starting from {} (base: {}). Final streakCount: {}",
1151+
toUpdate.size(), userId, recoveryStartDate, baseStreakCount, streakCount);
1152+
}
1153+
}
1154+
}

‎src/test/java/com/linglevel/api/streak/service/StreakServiceRecoveryTest.java‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,18 @@ private void setupMocks() {
524524
return sorted;
525525
});
526526

527+
// recalculateAllStreakCounts용 mock - startDate 이후 조회
528+
lenient().when(dailyCompletionRepository.findByUserIdAndCompletionDateGreaterThanEqualOrderByCompletionDateAsc(
529+
eq(TEST_USER_ID), any(LocalDate.class)))
530+
.thenAnswer(invocation -> {
531+
LocalDate startDate = invocation.getArgument(1);
532+
List<DailyCompletion> sorted = new ArrayList<>(completionMap.values());
533+
sorted.sort((a, b) -> a.getCompletionDate().compareTo(b.getCompletionDate()));
534+
return sorted.stream()
535+
.filter(dc -> !dc.getCompletionDate().isBefore(startDate))
536+
.toList();
537+
});
538+
527539
// saveAll mock
528540
lenient().when(dailyCompletionRepository.saveAll(anyList()))
529541
.thenAnswer(invocation -> {

0 commit comments

Comments
 (0)