Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat : 퀴즈 진행 API 구현 (#19) #20

Merged
merged 21 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5b16b27
Feat : UserService 기능 추가 (#19)
chanmin-00 Dec 30, 2024
751d5b8
Feat : Redis 설정 추가 (#19)
chanmin-00 Dec 30, 2024
59c4fa5
Feat : 퀴즈 진행 api 관련 DTO 추가 (#19)
chanmin-00 Dec 30, 2024
efdb0b2
Feat : 학습 세트 조회 함수 추가 (#19)
chanmin-00 Dec 30, 2024
1ba8d66
Feat : 퀴즈 진행 에러 코드 추가 (#19)
chanmin-00 Dec 30, 2024
83ced22
Feat : 퀴즈 인덱스 유효성 검사 어노테이션 추가 (#19)
chanmin-00 Dec 30, 2024
2ddc459
Feat : 퀴즈 진행 기능 추가 (#19)
chanmin-00 Dec 30, 2024
0dcb4b1
Feat : 퀴즈 진행 api 구현 (#19)
chanmin-00 Dec 30, 2024
07a4902
Chore : final 및 주석 추가
chanmin-00 Dec 30, 2024
7c41562
Refactor : 코드 리팩토링 및 필드 수정 (#15)
chanmin-00 Jan 8, 2025
949d5ed
Refactor : 코드 리팩토링 (#15)
chanmin-00 Jan 8, 2025
4fe6a93
Refactor : 코드 리팩토링 (#15)
chanmin-00 Jan 8, 2025
1230f96
Refactor : 코드 리팩토링 (#15)
chanmin-00 Jan 8, 2025
87a7759
Refactor : 메서드명 변경 (#15)
chanmin-00 Jan 8, 2025
8854af2
Merge branch 'develop' into feat/learning-#15
chanmin-00 Jan 8, 2025
a369d96
Refactor : 코드 리팩토링 (#15)
chanmin-00 Jan 8, 2025
f7105e6
Refactor : 패키지 수정 (#15)
chanmin-00 Jan 11, 2025
54c4548
Refactor : 기존의 선지 재활용 방식에서 사지선다 리스트를 가지고 있는 방식으로 수정 (#15)
chanmin-00 Jan 12, 2025
fa666ef
Refactor : 기존의 선지 재활용 방식에서 사지선다 리스트를 가지고 있는 방식으로 수정 (#15)
chanmin-00 Jan 12, 2025
a173eed
Refactor : Redis 키 중복 오류 수정 (#15)
chanmin-00 Jan 12, 2025
fdb4fa7
Refactor : 엑셀 예시 파일 수정 (#15)
chanmin-00 Jan 12, 2025
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
25 changes: 25 additions & 0 deletions src/main/java/com/ripple/BE/global/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.ripple.BE.global.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);

// Key serializer
template.setKeySerializer(new StringRedisSerializer());
// Value serializer
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());

return template;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,11 @@ public ResponseEntity<Object> handleUserException(final UserException e) {
return handleExceptionInternal(e.getErrorCode());
}


@ExceptionHandler(QuizException.class)
public ResponseEntity<Object> handleQuizException(final QuizException e) {
return handleExceptionInternal(e.getErrorCode());
}


@ExceptionHandler(LearningException.class)
public ResponseEntity<Object> handleLearningException(final LearningException e) {
return handleExceptionInternal(e.getErrorCode());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import com.ripple.BE.global.dto.response.ApiResponse;
import com.ripple.BE.learning.dto.ConceptListDTO;
import com.ripple.BE.learning.dto.response.ConceptListResponse;
import com.ripple.BE.learning.service.ConceptService;
import com.ripple.BE.learning.service.concept.ConceptService;
import com.ripple.BE.user.domain.CustomUserDetails;
import com.ripple.BE.user.domain.type.Level;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
Expand All @@ -15,6 +16,7 @@
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
Expand All @@ -28,9 +30,10 @@ public class ConceptController {
@Operation(summary = "개념 학습 세트 조회", description = "개념 학습 세트를 조회합니다.")
@GetMapping("/{learningSetId}/concepts")
public ResponseEntity<ApiResponse<Object>> getConcepts(
final @PathVariable("learningSetId") long learningSetId) {
final @PathVariable("learningSetId") long learningSetId,
final @RequestParam(defaultValue = "BEGINNER") Level level) {

ConceptListDTO conceptListDTO = conceptService.getConcepts(learningSetId);
ConceptListDTO conceptListDTO = conceptService.getConcepts(learningSetId, level);

return ResponseEntity.status(HttpStatus.OK)
.body(ApiResponse.from(ConceptListResponse.toConceptListResponse(conceptListDTO)));
Expand All @@ -40,9 +43,10 @@ public ResponseEntity<ApiResponse<Object>> getConcepts(
@PostMapping("/{learningSetId}/concepts/complete")
public ResponseEntity<ApiResponse<?>> completeConcept(
final @AuthenticationPrincipal CustomUserDetails currentUser,
final @PathVariable("learningSetId") long learningSetId) {
final @PathVariable("learningSetId") long learningSetId,
final @RequestParam(defaultValue = "BEGINNER") Level level) {

conceptService.completeConceptLearning(currentUser.getId(), learningSetId);
conceptService.completeConceptLearning(currentUser.getId(), learningSetId, level);
return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.EMPTY_RESPONSE);
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
package com.ripple.BE.learning.controller;

import com.ripple.BE.global.dto.response.ApiResponse;
import com.ripple.BE.learning.dto.LearningSetCompleteListDTO;
import com.ripple.BE.learning.dto.ProgressDTO;
import com.ripple.BE.learning.dto.request.LearningSetRequest;
import com.ripple.BE.learning.dto.UserLearningSetListDTO;
import com.ripple.BE.learning.dto.response.LearningSetPreviewListResponse;
import com.ripple.BE.learning.dto.response.LearningSetProgressResponse;
import com.ripple.BE.learning.service.LearningAdminService;
import com.ripple.BE.learning.service.LearningSetService;
import com.ripple.BE.learning.service.learningset.LearningAdminService;
import com.ripple.BE.learning.service.learningset.LearningSetService;
import com.ripple.BE.user.domain.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

Expand All @@ -41,31 +35,15 @@ public ResponseEntity<ApiResponse<?>> saveLearningSetsByExcel() {
@Operation(summary = "레벨별 학습 세트 조회", description = "레벨별 전체 학습 세트를 조회합니다.")
@PostMapping
public ResponseEntity<ApiResponse<Object>> getLearningSets(
final @AuthenticationPrincipal CustomUserDetails currentUser,
final @RequestBody @Valid LearningSetRequest request) {

LearningSetCompleteListDTO learningSetCompleteListDTO =
learningSetService.getLearningSetPreviewList(currentUser.getId(), request.level());

return ResponseEntity.status(HttpStatus.OK)
.body(
ApiResponse.from(
LearningSetPreviewListResponse.toLearningSetPreviewListResponse(
learningSetCompleteListDTO)));
}

@Operation(
summary = "학습 세트 진도율 조회",
description = "학습 세트의 진도율을 조회합니다. 100퍼센트 중 몇 퍼센트를 완료했는지 반환합니다.")
@GetMapping("/progress")
public ResponseEntity<ApiResponse<Object>> getLearningSetProgress(
final @AuthenticationPrincipal CustomUserDetails currentUser) {

ProgressDTO progressDTO = learningSetService.getLearningSetCompletionRate(currentUser.getId());
UserLearningSetListDTO userLearningSetListDTO =
learningSetService.getLearningSetPreviewList(currentUser.getId());

return ResponseEntity.status(HttpStatus.OK)
.body(
ApiResponse.from(
LearningSetProgressResponse.toLearningSetProgressResponse(progressDTO)));
LearningSetPreviewListResponse.toLearningSetPreviewListResponse(
userLearningSetListDTO)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.ripple.BE.learning.controller;

import com.ripple.BE.global.dto.response.ApiResponse;
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.QuizResultResponse;
import com.ripple.BE.learning.service.quiz.QuizService;
import com.ripple.BE.user.domain.CustomUserDetails;
import com.ripple.BE.user.domain.type.Level;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/v1/learning")
@Tag(name = "Learning", description = "학습 API")
public class QuizController {

private final QuizService quizService;

@Operation(summary = "퀴즈 시작", description = "퀴즈를 시작합니다.")
@PostMapping("/{learningSetId}/quizzes")
public ResponseEntity<ApiResponse<Object>> startQuiz(
final @AuthenticationPrincipal CustomUserDetails currentUser,
final @PathVariable("learningSetId") long learningSetId,
final @RequestParam(defaultValue = "BEGINNER") Level level) {

QuizListDTO quizListDTO = quizService.startQuiz(currentUser.getId(), learningSetId, level);

return ResponseEntity.status(HttpStatus.OK)
.body(ApiResponse.from(QuizListResponse.toQuizListResponse(quizListDTO)));
}

@Operation(summary = "퀴즈 제출", description = "퀴즈를 제출 후 정답 여부와 해설을 반환합니다.")
@PostMapping("/{learningSetId}/quizzes/{quizId}")
public ResponseEntity<ApiResponse<Object>> submitAnswer(
final @AuthenticationPrincipal CustomUserDetails currentUser,
final @PathVariable("learningSetId") long learningSetId,
final @PathVariable("quizId") long quizId,
final @RequestBody @Valid SubmitAnswerRequest request) {

QuizResultDTO quizResultDTO =
quizService.submitAnswer(currentUser.getId(), quizId, request.answerIndex());

return ResponseEntity.status(HttpStatus.OK)
.body(ApiResponse.from(QuizResultResponse.toQuizResultResponse(quizResultDTO)));
}

@Operation(summary = "퀴즈 완료", description = "퀴즈를 완료합니다.")
@PostMapping("/{learningSetId}/quizzes/end")
public ResponseEntity<ApiResponse<Object>> finishQuiz(
final @AuthenticationPrincipal CustomUserDetails currentUser,
final @PathVariable("learningSetId") long learningSetId,
final @RequestParam(defaultValue = "BEGINNER") Level level) {

quizService.finishQuiz(currentUser.getId(), learningSetId, level);

return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.from(ApiResponse.EMPTY_RESPONSE));
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.ripple.BE.learning.domain;
package com.ripple.BE.learning.domain.concept;

import com.ripple.BE.global.entity.BaseEntity;
import com.ripple.BE.learning.domain.learningset.LearningSet;
import com.ripple.BE.learning.dto.ConceptDTO;
import com.ripple.BE.user.domain.type.Level;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
Expand All @@ -28,24 +32,27 @@ public class Concept extends BaseEntity {
@Column(name = "concept_id")
private Long conceptId;

@Enumerated(EnumType.STRING)
@Column(name = "level")
private Level level;

@Size(max = 255)
@Column(name = "name", nullable = false)
private String name;

@Size(max = 255)
@Column(name = "explanation", nullable = false)
@Column(name = "explanation", nullable = false, columnDefinition = "TEXT")
private String explanation;

@Size(max = 255)
@Column(name = "example")
@Column(name = "example", columnDefinition = "TEXT")
private String example;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "learning_set_id")
private LearningSet learningSet;

@Builder
public Concept(String name, String explanation, String example) {
public Concept(Level level, String name, String explanation, String example) {
this.level = level;
this.name = name;
this.explanation = explanation;
this.example = example;
Expand All @@ -54,9 +61,9 @@ public Concept(String name, String explanation, String example) {
public static Concept toConcept(final ConceptDTO conceptDTO) {

return Concept.builder()
.level(conceptDTO.level())
.name(conceptDTO.name())
.explanation(conceptDTO.explanation())
.example(conceptDTO.example())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package com.ripple.BE.learning.domain;
package com.ripple.BE.learning.domain.learningset;

import com.ripple.BE.global.entity.BaseEntity;
import com.ripple.BE.learning.domain.concept.Concept;
import com.ripple.BE.learning.domain.quiz.Quiz;
import com.ripple.BE.learning.dto.LearningSetDTO;
import com.ripple.BE.user.domain.type.Level;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
Expand All @@ -34,36 +33,18 @@ public class LearningSet extends BaseEntity {
@Column(name = "name", nullable = false)
private String name;

@Column(name = "description")
private String description;

@Enumerated(EnumType.STRING)
@Column(name = "level")
private Level level;

@Column(name = "learning_set_num", nullable = false)
private String learningSetNum; // 학습 세트 번호

@OneToMany(mappedBy = "learningSet", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Concept> concepts = new ArrayList<>();

@OneToMany(mappedBy = "learningSet", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Quiz> quizzes = new ArrayList<>();

@Builder
public LearningSet(String name, String description, Level level, String learningSetNum) {
public LearningSet(String name) {
this.name = name;
this.description = description;
this.level = level;
this.learningSetNum = learningSetNum;
}

public static LearningSet toLearningSet(final LearningSetDTO learningSetDTO) {
return LearningSet.builder()
.name(learningSetDTO.name())
.description(learningSetDTO.description())
.level(learningSetDTO.level())
.learningSetNum(learningSetDTO.learningSetNum())
.build();
return LearningSet.builder().name(learningSetDTO.name()).build();
}
}
Loading