Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ springBoot {
}

group = 'com.linglevel'
version = '3.1.3-SNAPSHOT'
version = '3.1.4-SNAPSHOT'

java {
toolchain {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import com.linglevel.api.admin.dto.NotificationBroadcastResponse;
import com.linglevel.api.admin.dto.NotificationSendResponse;
import com.linglevel.api.admin.dto.NotificationSendRequest;
import com.linglevel.api.admin.dto.RecoverStreakRequest;
import com.linglevel.api.admin.dto.RecoverStreakResponse;
import com.linglevel.api.admin.dto.ResetTodayStreakRequest;
import com.linglevel.api.fcm.dto.FcmMessageRequest;
import com.linglevel.api.admin.dto.UpdateChunkRequest;
Expand All @@ -24,6 +26,8 @@
import com.linglevel.api.content.article.dto.ArticleOriginResponse;
import com.linglevel.api.content.article.dto.GetArticleOriginsRequest;
import com.linglevel.api.content.article.service.ArticleService;
import com.linglevel.api.streak.entity.UserStudyReport;
import com.linglevel.api.streak.service.StreakService;
import com.linglevel.api.user.entity.User;
import com.linglevel.api.user.repository.UserRepository;
import com.linglevel.api.user.ticket.service.TicketService;
Expand Down Expand Up @@ -57,6 +61,7 @@ public class AdminController {
private final TicketService ticketService;
private final UserRepository userRepository;
private final ArticleService articleService;
private final StreakService streakService;

@Operation(summary = "책 청크 수정", description = "어드민 권한으로 특정 책의 청크 내용을 수정합니다.")
@PutMapping("/books/{bookId}/chapters/{chapterId}/chunks/{chunkId}")
Expand Down Expand Up @@ -199,6 +204,40 @@ public ResponseEntity<MessageResponse> resetTodayStreak(@Valid @RequestBody Rese
return ResponseEntity.ok(new MessageResponse("User " + request.getUserId() + "'s streak status for today has been reset."));
}

@Operation(summary = "스트릭 복구", description = "어드민 권한으로 특정 사용자의 누락된 스트릭을 복구합니다. MISSED 날짜는 COMPLETED로 변경하고, FREEZE_USED는 COMPLETED로 변경하며 프리즈를 보상합니다. 복구 범위 이후 날짜들도 프리즈를 사용하여 최대한 연결합니다.")
@PostMapping("/streaks/recover")
public ResponseEntity<RecoverStreakResponse> recoverStreak(
@Parameter(description = "스트릭 복구 요청", required = true) @Valid @RequestBody RecoverStreakRequest request) {

log.info("Admin recovering streak - userId: {}, startDate: {}, endDate: {}",
request.getUserId(), request.getStartDate(), request.getEndDate());

// 사용자 존재 여부 확인
User user = userRepository.findById(request.getUserId())
.orElseThrow(() -> new CommonException(CommonErrorCode.RESOURCE_NOT_FOUND, "User not found."));

// 스트릭 복구 실행
streakService.recoverStreak(request.getUserId(), request.getStartDate(), request.getEndDate());

// 복구 후 UserStudyReport 조회
UserStudyReport updatedReport = streakService.recalculateUserStudyReport(request.getUserId());

RecoverStreakResponse response = RecoverStreakResponse.builder()
.message("Streak recovered successfully.")
.userId(request.getUserId())
.startDate(request.getStartDate())
.endDate(request.getEndDate())
.currentStreak(updatedReport.getCurrentStreak())
.longestStreak(updatedReport.getLongestStreak())
.lastCompletionDate(updatedReport.getLastCompletionDate())
.build();

log.info("Streak recovery completed for user {} - currentStreak: {}, longestStreak: {}",
request.getUserId(), updatedReport.getCurrentStreak(), updatedReport.getLongestStreak());

return ResponseEntity.ok(response);
}

@ExceptionHandler(TicketException.class)
public ResponseEntity<ExceptionResponse> handleTicketException(TicketException e) {
log.error("Admin Ticket Exception: {}", e.getMessage());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.linglevel.api.admin.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDate;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "스트릭 복구 요청")
public class RecoverStreakRequest {

@NotBlank(message = "userId는 필수입니다.")
@Schema(description = "사용자 ID", example = "user123", required = true)
private String userId;

@NotNull(message = "startDate는 필수입니다.")
@Schema(description = "복구 시작 날짜 (YYYY-MM-DD)", example = "2025-01-10", required = true)
private LocalDate startDate;

@NotNull(message = "endDate는 필수입니다.")
@Schema(description = "복구 종료 날짜 (YYYY-MM-DD)", example = "2025-01-15", required = true)
private LocalDate endDate;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.linglevel.api.admin.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDate;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "스트릭 복구 응답")
public class RecoverStreakResponse {

@Schema(description = "응답 메시지", example = "Streak recovered successfully.")
private String message;

@Schema(description = "사용자 ID", example = "user123")
private String userId;

@Schema(description = "복구 시작 날짜", example = "2025-01-10")
private LocalDate startDate;

@Schema(description = "복구 종료 날짜", example = "2025-01-15")
private LocalDate endDate;

@Schema(description = "복구 후 현재 스트릭", example = "15")
private Integer currentStreak;

@Schema(description = "복구 후 최장 스트릭", example = "20")
private Integer longestStreak;

@Schema(description = "마지막 완료일", example = "2025-01-15")
private LocalDate lastCompletionDate;
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ public interface DailyCompletionRepository extends MongoRepository<DailyCompleti
*/
@Query("{ 'userId': ?0, 'completionDate': { $gte: ?1 } }")
List<DailyCompletion> findByUserIdAndCompletionDateAfter(String userId, LocalDate startDate);

/**
* 특정 사용자의 모든 DailyCompletion을 날짜 오름차순으로 조회
*/
List<DailyCompletion> findByUserIdOrderByCompletionDateAsc(String userId);
}
Loading
Loading