From 0c8bc8443c496589c2c7d9a710b8b538e072ff77 Mon Sep 17 00:00:00 2001 From: chaen-ing Date: Wed, 22 Jan 2025 01:03:09 +0900 Subject: [PATCH 1/5] =?UTF-8?q?Feat=20:=20=EA=B0=9C=EB=85=90=20=ED=95=99?= =?UTF-8?q?=EC=8A=B5=20=EC=A0=80=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ConceptController.java | 10 +++++ .../learning/domain/concept/ConceptScrap.java | 39 +++++++++++++++++++ .../repository/ConceptScrapRepository.java | 8 ++++ .../service/concept/ConceptService.java | 20 ++++++++++ 4 files changed, 77 insertions(+) create mode 100644 src/main/java/com/ripple/BE/learning/domain/concept/ConceptScrap.java create mode 100644 src/main/java/com/ripple/BE/learning/repository/ConceptScrapRepository.java diff --git a/src/main/java/com/ripple/BE/learning/controller/ConceptController.java b/src/main/java/com/ripple/BE/learning/controller/ConceptController.java index 8fa7507..1ecf161 100644 --- a/src/main/java/com/ripple/BE/learning/controller/ConceptController.java +++ b/src/main/java/com/ripple/BE/learning/controller/ConceptController.java @@ -49,4 +49,14 @@ public ResponseEntity> completeConcept( conceptService.completeConceptLearning(currentUser.getId(), learningSetId, level); return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.EMPTY_RESPONSE); } + + @Operation(summary = "개념 학습 저장", description = "개념 학습을 저장합니다.") + @PostMapping("/learning/{conceptId}/scrap") + public ResponseEntity> scrapConcept( + final @AuthenticationPrincipal CustomUserDetails currentUser, + final @PathVariable("conceptId") long conceptId) { + + conceptService.scrapConcept(currentUser.getId(), conceptId); + return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.EMPTY_RESPONSE); + } } diff --git a/src/main/java/com/ripple/BE/learning/domain/concept/ConceptScrap.java b/src/main/java/com/ripple/BE/learning/domain/concept/ConceptScrap.java new file mode 100644 index 0000000..74b9637 --- /dev/null +++ b/src/main/java/com/ripple/BE/learning/domain/concept/ConceptScrap.java @@ -0,0 +1,39 @@ +package com.ripple.BE.learning.domain.concept; + +import com.ripple.BE.user.domain.User; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Table(name = "concept_scraps") +@Getter +@Builder +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class ConceptScrap { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "concept_id") + private Concept concept; +} diff --git a/src/main/java/com/ripple/BE/learning/repository/ConceptScrapRepository.java b/src/main/java/com/ripple/BE/learning/repository/ConceptScrapRepository.java new file mode 100644 index 0000000..187268a --- /dev/null +++ b/src/main/java/com/ripple/BE/learning/repository/ConceptScrapRepository.java @@ -0,0 +1,8 @@ +package com.ripple.BE.learning.repository; + +import com.ripple.BE.learning.domain.concept.ConceptScrap; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ConceptScrapRepository extends JpaRepository {} diff --git a/src/main/java/com/ripple/BE/learning/service/concept/ConceptService.java b/src/main/java/com/ripple/BE/learning/service/concept/ConceptService.java index 4df880a..0087081 100644 --- a/src/main/java/com/ripple/BE/learning/service/concept/ConceptService.java +++ b/src/main/java/com/ripple/BE/learning/service/concept/ConceptService.java @@ -1,12 +1,14 @@ package com.ripple.BE.learning.service.concept; import com.ripple.BE.learning.domain.concept.Concept; +import com.ripple.BE.learning.domain.concept.ConceptScrap; import com.ripple.BE.learning.domain.learningset.LearningSet; import com.ripple.BE.learning.domain.learningset.UserLearningSet; import com.ripple.BE.learning.dto.ConceptListDTO; import com.ripple.BE.learning.exception.LearningException; import com.ripple.BE.learning.exception.errorcode.LearningErrorCode; import com.ripple.BE.learning.repository.ConceptRepository; +import com.ripple.BE.learning.repository.ConceptScrapRepository; import com.ripple.BE.learning.repository.UserLearningSetRepository; import com.ripple.BE.learning.service.learningset.LearningSetService; import com.ripple.BE.user.domain.User; @@ -28,6 +30,7 @@ public class ConceptService { private final UserLearningSetRepository userLearningSetRepository; private final ConceptRepository conceptRepository; + private final ConceptScrapRepository conceptScrapRepository; /** * 학습 세트의 개념 목록을 조회 @@ -66,4 +69,21 @@ public void completeConceptLearning( userService.updateCompletedCountByLevel(user, level); } } + + /** + * 개념 스크랩 + * + * @param userId + * @param conceptId + */ + @Transactional + public void scrapConcept(final long userId, final long conceptId) { + User user = userService.findUserById(userId); + Concept concept = + conceptRepository + .findById(conceptId) + .orElseThrow(() -> new LearningException(LearningErrorCode.CONCEPT_NOT_FOUND)); + + conceptScrapRepository.save(ConceptScrap.builder().user(user).concept(concept).build()); + } } From 9e871c5935152fbdc2f8aad59b0307a1f6f6f71f Mon Sep 17 00:00:00 2001 From: chaen-ing Date: Wed, 22 Jan 2025 01:09:59 +0900 Subject: [PATCH 2/5] =?UTF-8?q?Feat=20:=20=ED=80=B4=EC=A6=88=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../learning/controller/QuizController.java | 10 ++++++++++ .../repository/QuizScrapRepository.java | 8 ++++++++ .../BE/learning/service/quiz/QuizService.java | 20 +++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 src/main/java/com/ripple/BE/learning/repository/QuizScrapRepository.java diff --git a/src/main/java/com/ripple/BE/learning/controller/QuizController.java b/src/main/java/com/ripple/BE/learning/controller/QuizController.java index cb49832..58eea9e 100644 --- a/src/main/java/com/ripple/BE/learning/controller/QuizController.java +++ b/src/main/java/com/ripple/BE/learning/controller/QuizController.java @@ -70,4 +70,14 @@ public ResponseEntity> finishQuiz( return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.from(ApiResponse.EMPTY_RESPONSE)); } + + @Operation(summary = "퀴즈 저장", description = "퀴즈를 저장합니다.") + @PostMapping("/learning/quiz/{quizId}/scrap") + public ResponseEntity> scrapQuiz( + final @AuthenticationPrincipal CustomUserDetails currentUser, + final @PathVariable("quizId") long conceptId) { + + quizService.scrapQuiz(currentUser.getId(), conceptId); + return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.EMPTY_RESPONSE); + } } diff --git a/src/main/java/com/ripple/BE/learning/repository/QuizScrapRepository.java b/src/main/java/com/ripple/BE/learning/repository/QuizScrapRepository.java new file mode 100644 index 0000000..8ebdaf3 --- /dev/null +++ b/src/main/java/com/ripple/BE/learning/repository/QuizScrapRepository.java @@ -0,0 +1,8 @@ +package com.ripple.BE.learning.repository; + +import com.ripple.BE.learning.domain.quiz.QuizScrap; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface QuizScrapRepository extends JpaRepository {} diff --git a/src/main/java/com/ripple/BE/learning/service/quiz/QuizService.java b/src/main/java/com/ripple/BE/learning/service/quiz/QuizService.java index c0f01f3..dcf3b56 100644 --- a/src/main/java/com/ripple/BE/learning/service/quiz/QuizService.java +++ b/src/main/java/com/ripple/BE/learning/service/quiz/QuizService.java @@ -6,13 +6,16 @@ import com.ripple.BE.learning.domain.learningset.UserLearningSet; import com.ripple.BE.learning.domain.quiz.FailQuiz; import com.ripple.BE.learning.domain.quiz.Quiz; +import com.ripple.BE.learning.domain.quiz.QuizScrap; import com.ripple.BE.learning.domain.type.Type; import com.ripple.BE.learning.dto.QuizDTO; import com.ripple.BE.learning.dto.QuizListDTO; import com.ripple.BE.learning.dto.QuizResultDTO; import com.ripple.BE.learning.exception.LearningException; +import com.ripple.BE.learning.exception.QuizException; import com.ripple.BE.learning.exception.errorcode.LearningErrorCode; import com.ripple.BE.learning.repository.QuizRepository; +import com.ripple.BE.learning.repository.QuizScrapRepository; import com.ripple.BE.learning.repository.UserLearningSetRepository; import com.ripple.BE.learning.service.learningset.LearningSetService; import com.ripple.BE.user.domain.User; @@ -32,6 +35,7 @@ public class QuizService { private final UserLearningSetRepository userLearningSetRepository; private final QuizRepository quizRepository; + private final QuizScrapRepository quizScrapRepository; private final QuizRedisService quizRedisService; private final LearningSetService learningSetService; @@ -148,4 +152,20 @@ private QuizDTO getQuizDTO(final QuizListDTO quizListDTO, final long quizId) { .findFirst() .orElseThrow(() -> new LearningException(QUIZ_PROGRESS_NOT_FOUND)); } + + /** + * 퀴즈 스크랩 + * + * @param userId + * @param quizId + */ + @Transactional + public void scrapQuiz(final long userId, final long quizId) { + + User user = userService.findUserById(userId); + Quiz quiz = + quizRepository.findById(quizId).orElseThrow(() -> new QuizException(QUIZ_NOT_FOUND)); + + quizScrapRepository.save(QuizScrap.builder().user(user).quiz(quiz).build()); + } } From b0ef671d926b56a250293e5dc9a83bc1dd32c060 Mon Sep 17 00:00:00 2001 From: chaen-ing Date: Wed, 22 Jan 2025 01:33:21 +0900 Subject: [PATCH 3/5] =?UTF-8?q?Feat=20:=20=EA=B0=9C=EB=B3=84=20=ED=80=B4?= =?UTF-8?q?=EC=A6=88=20=EC=A1=B0=ED=9A=8C=20=EC=B6=94=EA=B0=80=20(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BE/learning/controller/QuizController.java | 14 ++++++++++++++ .../BE/learning/service/quiz/QuizService.java | 12 ++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/main/java/com/ripple/BE/learning/controller/QuizController.java b/src/main/java/com/ripple/BE/learning/controller/QuizController.java index 58eea9e..67efff3 100644 --- a/src/main/java/com/ripple/BE/learning/controller/QuizController.java +++ b/src/main/java/com/ripple/BE/learning/controller/QuizController.java @@ -1,10 +1,12 @@ package com.ripple.BE.learning.controller; import com.ripple.BE.global.dto.response.ApiResponse; +import com.ripple.BE.learning.dto.QuizDTO; import com.ripple.BE.learning.dto.QuizListDTO; import com.ripple.BE.learning.dto.QuizResultDTO; import com.ripple.BE.learning.dto.request.SubmitAnswerRequest; import com.ripple.BE.learning.dto.response.QuizListResponse; +import com.ripple.BE.learning.dto.response.QuizResponse; import com.ripple.BE.learning.dto.response.QuizResultResponse; import com.ripple.BE.learning.service.quiz.QuizService; import com.ripple.BE.user.domain.CustomUserDetails; @@ -16,6 +18,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -80,4 +83,15 @@ public ResponseEntity> scrapQuiz( quizService.scrapQuiz(currentUser.getId(), conceptId); return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.EMPTY_RESPONSE); } + + @Operation(summary = "개별 퀴즈 조회", description = "개별 퀴즈를 조회합니다.") + @GetMapping("/learning/quiz/{quizId}") + public ResponseEntity> getSingleQuiz( + final @PathVariable("quizId") long quizId) { + + QuizDTO quizDTO = quizService.getSingleQuiz(quizId); + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.from(QuizResponse.toQuizResponse(quizDTO))); + } } diff --git a/src/main/java/com/ripple/BE/learning/service/quiz/QuizService.java b/src/main/java/com/ripple/BE/learning/service/quiz/QuizService.java index dcf3b56..c6ad39e 100644 --- a/src/main/java/com/ripple/BE/learning/service/quiz/QuizService.java +++ b/src/main/java/com/ripple/BE/learning/service/quiz/QuizService.java @@ -31,6 +31,7 @@ @RequiredArgsConstructor @Service @Slf4j +@Transactional(readOnly = true) public class QuizService { private final UserLearningSetRepository userLearningSetRepository; @@ -168,4 +169,15 @@ public void scrapQuiz(final long userId, final long quizId) { quizScrapRepository.save(QuizScrap.builder().user(user).quiz(quiz).build()); } + + /** + * 개별 퀴즈 조회 + * + * @param quizId + * @return 퀴즈 목록 반환 + */ + public QuizDTO getSingleQuiz(final long quizId) { + Quiz quiz = getQuizById(quizId); + return QuizDTO.toQuizDTO(quiz); + } } From 7b9f3466917f7006463ac6e3a127c335e1f4beff Mon Sep 17 00:00:00 2001 From: chaen-ing Date: Wed, 22 Jan 2025 03:13:26 +0900 Subject: [PATCH 4/5] =?UTF-8?q?Feat=20:=20=EA=B0=9C=EB=B3=84=20=ED=80=B4?= =?UTF-8?q?=EC=A6=88=20=EC=A0=9C=EC=B6=9C=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BE/learning/service/quiz/QuizService.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/ripple/BE/learning/service/quiz/QuizService.java b/src/main/java/com/ripple/BE/learning/service/quiz/QuizService.java index c6ad39e..782ea3a 100644 --- a/src/main/java/com/ripple/BE/learning/service/quiz/QuizService.java +++ b/src/main/java/com/ripple/BE/learning/service/quiz/QuizService.java @@ -88,17 +88,17 @@ public QuizResultDTO submitAnswer(final long userId, final long quizId, final in QuizListDTO quizListDTO = quizRedisService.fetchFromRedis(userId, QUESTION_TYPE, QuizListDTO.class); + + // 마이페이지의 저장한 퀴즈에서 다시풀기 진행 시 (퀴즈 진행 redis 데이터가 없을 경우) if (quizListDTO == null) { - throw new LearningException(QUIZ_PROGRESS_NOT_FOUND); + Quiz quiz = getQuizById(quizId); + boolean isCorrect = isCorrectAnswer(QuizDTO.toQuizDTO(quiz), answerIndex); + + return QuizResultDTO.toQuizResultDTO(isCorrect, quiz.getExplanation()); } QuizDTO quizDTO = getQuizDTO(quizListDTO, quizId); // 퀴즈 정보 가져오기 - - boolean isCorrect = - quizDTO - .answer() - .trim() - .equals(quizDTO.choiceList().choices().get(answerIndex).content().trim()); + boolean isCorrect = isCorrectAnswer(quizDTO, answerIndex); if (!isCorrect) { quizRedisService.saveToRedisList(userId, WRONG_ANSWER_TYPE, quizId); // 오답 리스트에 추가 @@ -180,4 +180,12 @@ public QuizDTO getSingleQuiz(final long quizId) { Quiz quiz = getQuizById(quizId); return QuizDTO.toQuizDTO(quiz); } + + // 정답 여부 확인 + private boolean isCorrectAnswer(final QuizDTO quizDTO, final int answerIndex) { + return quizDTO + .answer() + .trim() + .equals(quizDTO.choiceList().choices().get(answerIndex).content().trim()); + } } From 143c7fc9a6ecf05d1bd362d8e35bc58f9791a7e7 Mon Sep 17 00:00:00 2001 From: chaen-ing Date: Wed, 22 Jan 2025 22:52:45 +0900 Subject: [PATCH 5/5] =?UTF-8?q?Debug=20:=20=EB=B3=80=EC=88=98=EB=AA=85=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ripple/BE/learning/controller/QuizController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/ripple/BE/learning/controller/QuizController.java b/src/main/java/com/ripple/BE/learning/controller/QuizController.java index 67efff3..c34195e 100644 --- a/src/main/java/com/ripple/BE/learning/controller/QuizController.java +++ b/src/main/java/com/ripple/BE/learning/controller/QuizController.java @@ -78,9 +78,9 @@ public ResponseEntity> finishQuiz( @PostMapping("/learning/quiz/{quizId}/scrap") public ResponseEntity> scrapQuiz( final @AuthenticationPrincipal CustomUserDetails currentUser, - final @PathVariable("quizId") long conceptId) { + final @PathVariable("quizId") long quizId) { - quizService.scrapQuiz(currentUser.getId(), conceptId); + quizService.scrapQuiz(currentUser.getId(), quizId); return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.EMPTY_RESPONSE); }