diff --git a/src/main/java/GDGoC/team_24/domain/quiz/controller/QuizController.java b/src/main/java/GDGoC/team_24/domain/quiz/controller/QuizController.java index a2fa08a..151ddf4 100644 --- a/src/main/java/GDGoC/team_24/domain/quiz/controller/QuizController.java +++ b/src/main/java/GDGoC/team_24/domain/quiz/controller/QuizController.java @@ -2,6 +2,7 @@ import GDGoC.team_24.domain.quiz.converter.QuizConverter; import GDGoC.team_24.domain.quiz.domain.Quiz; +import GDGoC.team_24.domain.quiz.dto.CustomPage; import GDGoC.team_24.domain.quiz.dto.QuizRequestDto; import GDGoC.team_24.domain.quiz.dto.QuizResponseDto; import GDGoC.team_24.domain.quiz.service.QuizService; @@ -38,9 +39,9 @@ public ApiResponse answerQuiz(@RequestBody QuizRequestDto.answerQuiz re } - @Operation(summary = "퀴즈 조회 API", description = "지금까지 푼 퀴즈를 페이지네이션으로 조회합니다.") + @Operation(summary = "풀었던 퀴즈 조회 API", description = "지금까지 푼 퀴즈를 페이지네이션으로 조회합니다.") @GetMapping("/{userId}/answerlist") - public ApiResponse> listQuiz( + public ApiResponse> listQuiz( @PathVariable Long userId, Pageable pageable ) { @@ -54,9 +55,9 @@ public ApiResponse noAnswerQuiz (@PathVariable Long userId){ return ApiResponse.onSuccess(quizService.noAnswerQuiz(userId)); } - @Operation(summary = "안푼 퀴즈 조회 API", description = "지금까지 안푼 퀴즈를 페이지네이션으로 조회합니다.") + @Operation(summary = "안 푼 퀴즈 조회 API", description = "다시 풀 수 있는 퀴즈만 반환합니다.") @GetMapping("/{userId}/noanswerlist") - public ApiResponse> listNoQuiz( + public ApiResponse> listNoQuiz( @PathVariable Long userId, Pageable pageable ) { diff --git a/src/main/java/GDGoC/team_24/domain/quiz/domain/Quiz.java b/src/main/java/GDGoC/team_24/domain/quiz/domain/Quiz.java index 4a3f914..ec0e8f5 100644 --- a/src/main/java/GDGoC/team_24/domain/quiz/domain/Quiz.java +++ b/src/main/java/GDGoC/team_24/domain/quiz/domain/Quiz.java @@ -7,6 +7,8 @@ import lombok.NoArgsConstructor; import lombok.Setter; +import java.time.LocalDateTime; + @Entity @Getter @Setter @@ -27,6 +29,8 @@ public class Quiz extends BaseEntity { private Long quizAnswer; //퀴즈 답변 + private LocalDateTime nextRetryTime; + @ManyToOne(fetch = FetchType.LAZY) private User user; diff --git a/src/main/java/GDGoC/team_24/domain/quiz/dto/CustomPage.java b/src/main/java/GDGoC/team_24/domain/quiz/dto/CustomPage.java new file mode 100644 index 0000000..af86ef6 --- /dev/null +++ b/src/main/java/GDGoC/team_24/domain/quiz/dto/CustomPage.java @@ -0,0 +1,19 @@ +package GDGoC.team_24.domain.quiz.dto; + +import lombok.*; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CustomPage { + private List content; // 페이징된 실제 데이터 + private int pageNumber; // 현재 페이지 번호 + private int pageSize; // 페이지 크기 + private long totalElements; // 전체 요소 수 + private int totalPages; // 전체 페이지 수 + private boolean isLast; // 마지막 페이지 여부 + private boolean isFirst; // 첫 번째 페이지 여부 +} diff --git a/src/main/java/GDGoC/team_24/domain/quiz/dto/QuizResponseDto.java b/src/main/java/GDGoC/team_24/domain/quiz/dto/QuizResponseDto.java index 827e16d..f105d20 100644 --- a/src/main/java/GDGoC/team_24/domain/quiz/dto/QuizResponseDto.java +++ b/src/main/java/GDGoC/team_24/domain/quiz/dto/QuizResponseDto.java @@ -18,18 +18,16 @@ public static class QuizResponseDTO{ private LocalDateTime createdAt; } - @Builder @Getter - @Setter - @NoArgsConstructor - @AllArgsConstructor + @Builder public static class quizList { private Long id; - private String question; // 퀴즈 - private List options; // 선택지 목록 - private Long answer; // 정답 번호 - private Long quizAnswer; // 유저의 답변 번호 - private boolean isCorrect; // 맞/틀 여부 + private String question; + private List options; + private Long answer; + private Long quizAnswer; + private boolean isCorrect; + private LocalDateTime solvedAt; // 푼 날짜 추가 } // 선택지 DTO diff --git a/src/main/java/GDGoC/team_24/domain/quiz/repository/QuizRepository.java b/src/main/java/GDGoC/team_24/domain/quiz/repository/QuizRepository.java index e4034b6..8d3a501 100644 --- a/src/main/java/GDGoC/team_24/domain/quiz/repository/QuizRepository.java +++ b/src/main/java/GDGoC/team_24/domain/quiz/repository/QuizRepository.java @@ -5,8 +5,11 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.time.LocalDateTime; import java.util.Optional; import java.util.List; @@ -14,5 +17,19 @@ public interface QuizRepository extends JpaRepository { Page findByUserAndIsCompleted(User user, Boolean isCompleted, Pageable pageable); List findByUser (User user); - boolean existsByUserAndIsCompleted(User user, Boolean isCompleted); + @Query("SELECT q FROM Quiz q " + + "WHERE q.user = :user AND q.isCompleted = true") + Page findSolvedQuizzes(@Param("user") User user, Pageable pageable); + @Query("SELECT q FROM Quiz q " + + "WHERE q.user = :user " + + "AND q.isCompleted = false " + + "AND (q.nextRetryTime IS NULL OR q.nextRetryTime <= CURRENT_TIMESTAMP)") + Page findUnsolvedQuizzes(@Param("user") User user, Pageable pageable); + @Query("SELECT CASE WHEN COUNT(q) > 0 THEN TRUE ELSE FALSE END " + + "FROM Quiz q WHERE q.user = :user AND q.isCompleted = :isCompleted " + + "AND (q.nextRetryTime IS NULL OR q.nextRetryTime <= CURRENT_TIMESTAMP)") + boolean existsByUserAndIsCompletedAndNextRetryTimeBefore( + @Param("user") User user, + @Param("isCompleted") Boolean isCompleted); + } diff --git a/src/main/java/GDGoC/team_24/domain/quiz/service/QuizService.java b/src/main/java/GDGoC/team_24/domain/quiz/service/QuizService.java index 63306f7..1c8151b 100644 --- a/src/main/java/GDGoC/team_24/domain/quiz/service/QuizService.java +++ b/src/main/java/GDGoC/team_24/domain/quiz/service/QuizService.java @@ -4,6 +4,7 @@ import GDGoC.team_24.domain.family.repository.FamilyRepository; import GDGoC.team_24.domain.quiz.domain.Quiz; import GDGoC.team_24.domain.quiz.domain.QuizOption; +import GDGoC.team_24.domain.quiz.dto.CustomPage; import GDGoC.team_24.domain.quiz.dto.QuizRequestDto; import GDGoC.team_24.domain.quiz.dto.QuizResponseDto; import GDGoC.team_24.domain.quiz.repository.QuizOptionRepository; @@ -17,6 +18,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -30,20 +32,17 @@ public class QuizService { private final UserRepository userRepository; public Quiz makeQuiz(QuizRequestDto.createQuiz request, Long familyId) { - // 퀴즈 생성 + Family family = familyRepository.findById(familyId) + .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); + + User user = family.getUser(); // 중복 조회 제거 + Quiz newQuiz = new Quiz(); newQuiz.setQuestion(request.getQuestion()); newQuiz.setAnswer(request.getAnswer()); newQuiz.setCompleted(false); + newQuiz.setUser(user); // User 설정 - // Family 및 User 조회 - Family family = familyRepository.findById(familyId) - .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); - User user = userRepository.findById(family.getUser().getId()) - .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); - newQuiz.setUser(user); - - // 퀴즈 저장 Quiz savedQuiz = quizRepository.save(newQuiz); // 선택지 저장 @@ -60,66 +59,92 @@ public Quiz makeQuiz(QuizRequestDto.createQuiz request, Long familyId) { return savedQuiz; } + public Boolean answerQuiz(QuizRequestDto.answerQuiz request) { Quiz quiz = quizRepository.findById(request.getQuizId()) .orElseThrow(() -> new GeneralException(ErrorStatus.QUIZ_NOT_FOUND)); - // 정답 비교 + // 객관식 정답 비교 boolean isCorrect = quiz.getAnswer().equals(request.getAnswer()); quiz.setCorrect(isCorrect); - quiz.setCompleted(true); // 퀴즈 완료 상태로 변경 - quiz.setQuizAnswer(request.getAnswer()); // 사용자 답변 저장 - quizRepository.save(quiz); + quiz.setCompleted(true); // 퀴즈 완료 처리 + quiz.setQuizAnswer(request.getAnswer()); // 사용자 입력 저장 + + if (!isCorrect) { + // 틀린 경우: 하루 뒤에 다시 풀 수 있도록 설정 + quiz.setNextRetryTime(LocalDateTime.now().plusDays(1)); + } else { + // 맞은 경우: 7일 뒤에 다시 풀 수 있도록 설정 + quiz.setNextRetryTime(LocalDateTime.now().plusDays(7)); + } + quizRepository.save(quiz); return isCorrect; } + - - public Page readAllQuizzes(Long userId, Boolean solve, Pageable pageable) { - // User 조회 + public CustomPage readAllQuizzes(Long userId, Boolean solve, Pageable pageable) { User user = userRepository.findById(userId) .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); - // User가 푼/안 푼 퀴즈를 페이지네이션으로 조회 - Page quizzes = quizRepository.findByUserAndIsCompleted(user, solve, pageable); + Page quizzes = solve + ? quizRepository.findSolvedQuizzes(user, pageable) // 풀었던 퀴즈 조회 + : quizRepository.findUnsolvedQuizzes(user, pageable); // 안 푼 퀴즈 조회 if (quizzes.isEmpty()) { throw new GeneralException(ErrorStatus.QUIZ_NOT_FOUND); } - // 각 퀴즈와 해당 선택지 정보를 DTO로 변환 - return quizzes.map(quiz -> { - // 선택지 조회 - List options = quizOptionRepository.findByQuiz(quiz); - - // 선택지를 DTO로 변환 - List optionDtos = options.stream() - .map(option -> QuizResponseDto.QuizOptionDto.builder() - .number(option.getNumber()) - .text(option.getText()) - .build()) - .toList(); - - // 퀴즈와 선택지 정보를 DTO로 변환 - return QuizResponseDto.quizList.builder() - .id(quiz.getId()) - .question(quiz.getQuestion()) - .options(optionDtos) - .answer(quiz.getAnswer()) - .quizAnswer(quiz.getQuizAnswer()) - .isCorrect(quiz.isCorrect()) - .build(); - }); + // `Page` 객체를 `CustomPage`로 변환 + List quizList = quizzes.getContent().stream() + .map(quiz -> { + // 각 퀴즈의 선택지만 조회 + List options = quizOptionRepository.findByQuiz(quiz); + + // 선택지 DTO로 변환 + List optionDtos = options.stream() + .map(option -> QuizResponseDto.QuizOptionDto.builder() + .number(option.getNumber()) + .text(option.getText()) + .build()) + .toList(); + + // `solvedAt` 값을 퀴즈 상태에 따라 설정 + LocalDateTime solvedAt = quiz.isCompleted() ? quiz.getUpdatedAt() : null; + + // 퀴즈 DTO로 변환 + return QuizResponseDto.quizList.builder() + .id(quiz.getId()) + .question(quiz.getQuestion()) + .options(optionDtos) + .answer(quiz.getAnswer()) + .quizAnswer(quiz.getQuizAnswer()) + .isCorrect(quiz.isCorrect()) + .solvedAt(solvedAt) // 상태에 따라 `solvedAt` 값 설정 + .build(); + }) + .toList(); + + return CustomPage.builder() + .content(quizList) + .pageNumber(quizzes.getNumber()) + .pageSize(quizzes.getSize()) + .totalElements(quizzes.getTotalElements()) + .totalPages(quizzes.getTotalPages()) + .isLast(quizzes.isLast()) + .isFirst(quizzes.isFirst()) + .build(); } + public Boolean noAnswerQuiz(Long userId) { - // User 조회 User user = userRepository.findById(userId) .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); - // 안 푼 퀴즈 존재 여부 확인 - return quizRepository.existsByUserAndIsCompleted(user, false); + // 다음 풀 수 있는 퀴즈 존재 여부 확인 + return quizRepository.existsByUserAndIsCompletedAndNextRetryTimeBefore(user, false); } + }