diff --git a/src/main/java/com/_1/spring_rest_api/converter/QuestionGenerationConverter.java b/src/main/java/com/_1/spring_rest_api/converter/QuestionGenerationConverter.java index 91e709e..d668789 100644 --- a/src/main/java/com/_1/spring_rest_api/converter/QuestionGenerationConverter.java +++ b/src/main/java/com/_1/spring_rest_api/converter/QuestionGenerationConverter.java @@ -2,10 +2,16 @@ import com._1.spring_rest_api.api.dto.QuestionDto; import com._1.spring_rest_api.entity.Question; +import com._1.spring_rest_api.entity.Week; import com._1.spring_rest_api.repository.WeekRepository; +import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + /** * AI 생성 질문 DTO와 Question 엔티티 간의 변환을 담당하는 컨버터 */ @@ -39,13 +45,16 @@ public Question toEntity(QuestionDto dto) { .build(); } - /** - * AI가 생성한 질문 DTO를 특정 주차의 Question 엔티티로 변환 - * - * @param dto 질문 DTO - * @param weekId 주차 ID - * @return 주차에 연결된 Question 엔티티 - */ + public List createQuestionsForWeek(List dtos, Long weekId) { + if (dtos == null || dtos.isEmpty()) { + return new ArrayList<>(); + } + + return dtos.stream() + .map(dto -> createQuestionForWeek(dto, weekId)) + .collect(Collectors.toList()); + } + public Question createQuestionForWeek(QuestionDto dto, Long weekId) { if (dto == null) { return null; @@ -54,9 +63,14 @@ public Question createQuestionForWeek(QuestionDto dto, Long weekId) { Question question = toEntity(dto); if (weekId != null) { - weekRepository.findById(weekId).ifPresent(question::changeWeek); + Week week = weekRepository.findById(weekId) + .orElseThrow(() -> new EntityNotFoundException("Week not found with id: " + weekId)); + + week.addQuestion(question); } return question; } + + } \ No newline at end of file diff --git a/src/main/java/com/_1/spring_rest_api/entity/Course.java b/src/main/java/com/_1/spring_rest_api/entity/Course.java index 8d58d5f..1d52219 100644 --- a/src/main/java/com/_1/spring_rest_api/entity/Course.java +++ b/src/main/java/com/_1/spring_rest_api/entity/Course.java @@ -62,14 +62,6 @@ public void changeCreator(User creator) { } } - - public Course(User creator, String title, String description) { - this.creator = creator; - this.title = title; - this.description = description; - this.weeks = new ArrayList<>(); - } - public Course(Long id, User creator, String title, String description) { this.id = id; this.creator = creator; diff --git a/src/main/java/com/_1/spring_rest_api/entity/CustomQuiz.java b/src/main/java/com/_1/spring_rest_api/entity/CustomQuiz.java index ae413c2..5702dea 100644 --- a/src/main/java/com/_1/spring_rest_api/entity/CustomQuiz.java +++ b/src/main/java/com/_1/spring_rest_api/entity/CustomQuiz.java @@ -91,7 +91,7 @@ public void addQuestion(Question question) { } // 정적 팩토리 메서드를 통해 매핑 생성 및 양방향 연관관계 설정 - QuizQuestionMapping mapping = QuizQuestionMapping.create(this, question); + QuizQuestionMapping.create(this, question); // 질문 수 증가 this.updateTotalQuestions(this.totalQuestions + 1); diff --git a/src/main/java/com/_1/spring_rest_api/entity/QuizSession.java b/src/main/java/com/_1/spring_rest_api/entity/QuizSession.java index 81158bd..1883e72 100644 --- a/src/main/java/com/_1/spring_rest_api/entity/QuizSession.java +++ b/src/main/java/com/_1/spring_rest_api/entity/QuizSession.java @@ -37,6 +37,22 @@ public class QuizSession extends BaseTimeEntity { private LocalDateTime completedAt; + // User와 QuizSession 간의 양방향 연관관계 메서드 + public void changUser(User user) { + this.user = user; + if (user != null && !user.getQuizSessions().contains(this)) { + user.getQuizSessions().add(this); + } + } + + // CustomQuiz와 QuizSession 간의 양방향 연관관계 메서드 + public void changeQuiz(CustomQuiz quiz) { + this.quiz = quiz; + if (quiz != null && !quiz.getQuizSessions().contains(this)) { + quiz.getQuizSessions().add(this); + } + } + public static QuizSession create(User user, CustomQuiz quiz) { if (user == null) { throw new IllegalArgumentException("사용자는 null이 될 수 없습니다"); @@ -53,6 +69,29 @@ public static QuizSession create(User user, CustomQuiz quiz) { .build(); } + public UserAnswer createAnswer(String userAnswerText) { + Question currentQuestion = getCurrentQuestion(); + if (currentQuestion == null) { + throw new IllegalStateException("세션에 현재 질문이 없습니다"); + } + + boolean isCorrect = currentQuestion.isCorrectAnswer(userAnswerText); + + UserAnswer userAnswer = UserAnswer.builder() + .user(this.user) + .question(currentQuestion) + .userAnswer(userAnswerText) + .isCorrect(isCorrect) + .attemptCount(1) + .answeredAt(LocalDateTime.now()) + .build(); + + currentQuestion.addUserAnswer(userAnswer); + this.user.addUserAnswer(userAnswer); + + return userAnswer; + } + public void moveToNextQuestion() { this.currentQuestionIndex++; } @@ -86,38 +125,4 @@ public Question getNextQuestion() { public boolean isComplete() { return this.currentQuestionIndex >= this.quiz.getQuizQuestionMappings().size(); } - - public UserAnswer createAnswer(String userAnswerText) { - Question currentQuestion = getCurrentQuestion(); - if (currentQuestion == null) { - throw new IllegalStateException("세션에 현재 질문이 없습니다"); - } - - boolean isCorrect = currentQuestion.isCorrectAnswer(userAnswerText); - - return UserAnswer.builder() - .user(this.user) - .question(currentQuestion) - .userAnswer(userAnswerText) - .isCorrect(isCorrect) - .attemptCount(1) - .answeredAt(LocalDateTime.now()) - .build(); - } - - // User와 QuizSession 간의 양방향 연관관계 메서드 - public void changeUser(User user) { - this.user = user; - if (user != null && !user.getQuizSessions().contains(this)) { - user.getQuizSessions().add(this); - } - } - - // CustomQuiz와 QuizSession 간의 양방향 연관관계 메서드 - public void changeQuiz(CustomQuiz quiz) { - this.quiz = quiz; - if (quiz != null && !quiz.getQuizSessions().contains(this)) { - quiz.getQuizSessions().add(this); - } - } } \ No newline at end of file diff --git a/src/main/java/com/_1/spring_rest_api/entity/User.java b/src/main/java/com/_1/spring_rest_api/entity/User.java index a392a13..5e95afe 100644 --- a/src/main/java/com/_1/spring_rest_api/entity/User.java +++ b/src/main/java/com/_1/spring_rest_api/entity/User.java @@ -45,11 +45,11 @@ public class User extends BaseTimeEntity { private List userAnswers = new ArrayList<>(); // User와 UserKakao 간의 양방향 연관관계 메서드 - public void linkWithKakao(UserKakao userKakao) { + public void changeUserKakao(UserKakao userKakao) { this.userKakao = userKakao; // userKakao의 user 필드가 this가 아닌 경우에만 설정 if (userKakao.getUser() != this) { - userKakao.linkWithUser(this); + userKakao.changeUser(this); } } @@ -87,14 +87,14 @@ public void removeCourse(Course course) { public void addQuizSession(QuizSession session) { this.quizSessions.add(session); if (session.getUser() != this) { - session.changeUser(this); + session.changUser(this); } } public void removeQuizSession(QuizSession session) { this.quizSessions.remove(session); if (session.getUser() == this) { - session.changeUser(null); + session.changUser(null); } } @@ -121,10 +121,6 @@ public void updateName(String name) { this.name = name; } - public void updateUserKakao(UserKakao userKakao) { - this.userKakao = userKakao; - } - public static User createKakaoUser(String email, String name) { if (email == null || email.isEmpty()) { throw new IllegalArgumentException("이메일은 필수 값입니다."); diff --git a/src/main/java/com/_1/spring_rest_api/entity/UserKakao.java b/src/main/java/com/_1/spring_rest_api/entity/UserKakao.java index 684a573..7e0c3c6 100644 --- a/src/main/java/com/_1/spring_rest_api/entity/UserKakao.java +++ b/src/main/java/com/_1/spring_rest_api/entity/UserKakao.java @@ -39,11 +39,11 @@ public class UserKakao extends BaseTimeEntity { private LocalDateTime tokenExpiresAt; // User와 UserKakao 간의 양방향 연관관계 메서드 - public void linkWithUser(User user) { + public void changeUser(User user) { this.user = user; // user의 userKakao 필드가 this가 아닌 경우에만 설정 if (user.getUserKakao() != this) { - user.linkWithKakao(this); + user.changeUserKakao(this); } } @@ -65,12 +65,11 @@ public static UserKakao createKakaoAccountLink(User user, String kakaoId) { } UserKakao userKakao = UserKakao.builder() - .user(user) .kakaoAccountId(kakaoId) .build(); // 양방향 연관관계 설정 - user.updateUserKakao(userKakao); + userKakao.changeUser(user); return userKakao; } diff --git a/src/main/java/com/_1/spring_rest_api/repository/CustomQuizRepository.java b/src/main/java/com/_1/spring_rest_api/repository/CustomQuizRepository.java index e69830a..629de43 100644 --- a/src/main/java/com/_1/spring_rest_api/repository/CustomQuizRepository.java +++ b/src/main/java/com/_1/spring_rest_api/repository/CustomQuizRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.repository.query.Param; import java.util.List; +import java.util.Optional; public interface CustomQuizRepository extends JpaRepository { @@ -13,4 +14,18 @@ public interface CustomQuizRepository extends JpaRepository { @Query("SELECT q FROM CustomQuiz q WHERE q.creator.id = :creatorId ORDER BY q.createAt DESC") List findAllByCreatorId(@Param("creatorId") Long creatorId); + @Query("SELECT DISTINCT q FROM CustomQuiz q " + + "LEFT JOIN FETCH q.creator " + + "LEFT JOIN FETCH q.quizQuestionMappings qm " + + "LEFT JOIN FETCH qm.question " + + "WHERE q.id = :quizId") + Optional findByIdWithQuestions(@Param("quizId") Long quizId); + + @Query("SELECT DISTINCT q FROM CustomQuiz q " + + "LEFT JOIN FETCH q.quizWeekMappings wm " + + "LEFT JOIN FETCH wm.week w " + + "LEFT JOIN FETCH w.course " + + "WHERE q.id = :quizId") + Optional findByIdWithWeeks(@Param("quizId") Long quizId); + } diff --git a/src/main/java/com/_1/spring_rest_api/service/CourseService.java b/src/main/java/com/_1/spring_rest_api/service/CourseService.java index 382b9fc..c983715 100644 --- a/src/main/java/com/_1/spring_rest_api/service/CourseService.java +++ b/src/main/java/com/_1/spring_rest_api/service/CourseService.java @@ -4,6 +4,7 @@ import com._1.spring_rest_api.entity.User; import com._1.spring_rest_api.entity.Week; import com._1.spring_rest_api.repository.CourseRepository; +import com._1.spring_rest_api.repository.UserRepository; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -17,11 +18,21 @@ public class CourseService { private final CourseRepository courseRepository; + private final UserRepository userRepository; - public Long createCourse( - Long userId, String title, String description) { - // User entity 조회 후 넣어야 함. - 임시로 User() 사용 - Course course = courseRepository.save(new Course(new User(userId), title, description)); + public Long createCourse(Long userId, String title, String description) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException("User not found with id: " + userId)); + + Course course = Course.builder() + .creator(user) + .title(title) + .description(description) + .build(); + + user.addCourse(course); + + Course savedCourse = courseRepository.save(course); return course.getId(); } diff --git a/src/main/java/com/_1/spring_rest_api/service/QuestionCommandServiceImpl.java b/src/main/java/com/_1/spring_rest_api/service/QuestionCommandServiceImpl.java index 563cac6..02e57fc 100644 --- a/src/main/java/com/_1/spring_rest_api/service/QuestionCommandServiceImpl.java +++ b/src/main/java/com/_1/spring_rest_api/service/QuestionCommandServiceImpl.java @@ -41,14 +41,13 @@ public List generateAndSaveQuestions(Long weekId, int minQuestionCount) { List generatedQuestions = claudeService.generateQuestionsFromWeekTexts(weekId, minQuestionCount); - // 생성된 질문을 저장 (컨버터 활용) - List savedQuestionIds = generatedQuestions.stream() - .map(dto -> questionGenerationConverter.createQuestionForWeek(dto, weekId)) + List questions = questionGenerationConverter.createQuestionsForWeek( + generatedQuestions, weekId); + + return questions.stream() .map(questionRepository::save) .map(Question::getId) .collect(Collectors.toList()); - - return savedQuestionIds; } diff --git a/src/main/java/com/_1/spring_rest_api/service/QuizCommandServiceImpl.java b/src/main/java/com/_1/spring_rest_api/service/QuizCommandServiceImpl.java index f5618b9..e7a37f0 100644 --- a/src/main/java/com/_1/spring_rest_api/service/QuizCommandServiceImpl.java +++ b/src/main/java/com/_1/spring_rest_api/service/QuizCommandServiceImpl.java @@ -65,15 +65,11 @@ public Long startQuizSession(Long quizId, Long userId) { .orElseThrow(() -> new EntityNotFoundException("User not found with id: " + userId)); // 새 퀴즈 세션 생성 - QuizSession session = QuizSession.builder() - .user(user) - .quiz(quiz) - .currentQuestionIndex(0) // 첫 번째 질문부터 시작 - .build(); + QuizSession session = QuizSession.create(user, quiz); // 양방향 연관관계 설정 - session.changeUser(user); - session.changeQuiz(quiz); + user.addQuizSession(session); + quiz.addQuizSession(session); // 저장 및 ID 반환 QuizSession savedSession = quizSessionRepository.save(session); @@ -97,10 +93,7 @@ private void connectWeeksToQuiz(List weekIds, CustomQuiz quiz) { .build(); // 양방향 연관관계 설정 - mapping.changeQuiz(quiz); - mapping.changeWeek(week); - - quizWeekMappingRepository.save(mapping); + quiz.addQuizWeekMapping(mapping); } } @@ -124,8 +117,6 @@ private void processQuestions(CreateQuizRequest request, CustomQuiz quiz) { quiz.addQuestion(question); } - // 총 질문 수 업데이트 및 저장 - quiz.updateTotalQuestions(questions.size()); customQuizRepository.save(quiz); } diff --git a/src/main/java/com/_1/spring_rest_api/service/QuizQueryServiceImpl.java b/src/main/java/com/_1/spring_rest_api/service/QuizQueryServiceImpl.java index b6f76a6..19f81bf 100644 --- a/src/main/java/com/_1/spring_rest_api/service/QuizQueryServiceImpl.java +++ b/src/main/java/com/_1/spring_rest_api/service/QuizQueryServiceImpl.java @@ -29,9 +29,11 @@ public List getQuizzesByUserId(Long userId) { @Override public QuizDetailResponse getQuizDetail(Long quizId) { - CustomQuiz quiz = customQuizRepository.findById(quizId) + CustomQuiz quiz = customQuizRepository.findByIdWithQuestions(quizId) .orElseThrow(() -> new EntityNotFoundException("Quiz not found with id: " + quizId)); + customQuizRepository.findByIdWithWeeks(quizId); + return convertToDetailResponse(quiz); } diff --git a/src/main/java/com/_1/spring_rest_api/service/QuizSessionCommandServiceImpl.java b/src/main/java/com/_1/spring_rest_api/service/QuizSessionCommandServiceImpl.java index c3e0ce0..a614a6e 100644 --- a/src/main/java/com/_1/spring_rest_api/service/QuizSessionCommandServiceImpl.java +++ b/src/main/java/com/_1/spring_rest_api/service/QuizSessionCommandServiceImpl.java @@ -4,7 +4,6 @@ import com._1.spring_rest_api.api.dto.AnswerResponse; import com._1.spring_rest_api.converter.AnswerResponseConverter; import com._1.spring_rest_api.converter.QuestionConverter; -import com._1.spring_rest_api.entity.CustomQuiz; import com._1.spring_rest_api.entity.Question; import com._1.spring_rest_api.entity.QuizSession; import com._1.spring_rest_api.entity.UserAnswer; @@ -15,8 +14,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; - @Service @RequiredArgsConstructor @Transactional @@ -24,7 +21,6 @@ public class QuizSessionCommandServiceImpl implements QuizSessionCommandService private final QuizSessionRepository quizSessionRepository; private final UserAnswerRepository userAnswerRepository; - private final QuestionConverter questionConverter; private final AnswerResponseConverter answerResponseConverter; @Override @@ -40,8 +36,6 @@ public AnswerResponse answerQuestion(Long sessionId, AnswerRequest request) { // 3. 사용자 답변 생성 및 저장 UserAnswer userAnswer = session.createAnswer(request.getUserAnswer()); - userAnswer.changeUser(session.getUser()); - userAnswer.changeQuestion(currentQuestion); userAnswerRepository.save(userAnswer); // 4. 세션 상태 업데이트 diff --git a/src/main/java/com/_1/spring_rest_api/service/UserService.java b/src/main/java/com/_1/spring_rest_api/service/UserService.java index 1d3b425..e3d9472 100644 --- a/src/main/java/com/_1/spring_rest_api/service/UserService.java +++ b/src/main/java/com/_1/spring_rest_api/service/UserService.java @@ -18,6 +18,7 @@ @Service @RequiredArgsConstructor +@Transactional public class UserService { private final UserRepository userRepository; @@ -28,12 +29,8 @@ public User findByKakaoId(String kakaoId) { return userKakaoOpt.map(UserKakao::getUser).orElse(null); } - @Transactional public User createKakaoUser(String email, String name, String kakaoId) { - // Create new user User user = User.createKakaoUser(email, name); - - // Save user to get ID User savedUser = userRepository.save(user); // Create Kakao account link @@ -44,7 +41,6 @@ public User createKakaoUser(String email, String name, String kakaoId) { return savedUser; } - @Transactional public void updateKakaoTokens(User user, String accessToken, String refreshToken, LocalDateTime expiresAt) { UserKakao userKakao = user.getUserKakao(); diff --git a/src/main/java/com/_1/spring_rest_api/service/WeekService.java b/src/main/java/com/_1/spring_rest_api/service/WeekService.java index 67f4faf..886ec58 100644 --- a/src/main/java/com/_1/spring_rest_api/service/WeekService.java +++ b/src/main/java/com/_1/spring_rest_api/service/WeekService.java @@ -58,12 +58,11 @@ public WeekResponse createWeek(Long courseId, String title, Integer weekNumber) Week week = Week.builder() .title(title) .weekNumber(weekNumber) - .course(course) .build(); - week.changeCourse(course); - weekRepository.save(week); + course.addWeek(week); + weekRepository.save(week); return week.toWeekResponse(); } @@ -106,7 +105,7 @@ public void deleteWeek(Long weekId) { // 양방향 연관관계 정리 if (week.getCourse() != null) { - week.getCourse().getWeeks().remove(week); + week.getCourse().removeWeek(week); } weekRepository.delete(week); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 9b41b8b..c23707d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -21,4 +21,4 @@ springdoc.swagger-ui.path=/swagger-ui.html springdoc.api-docs.path=/v3/api-docs springdoc.swagger-ui.enabled=true springdoc.api-docs.enabled=true -springdoc.packages-to-scan=com._1.spring_rest_api.api.controller \ No newline at end of file +springdoc.packages-to-scan=com._1.spring_rest_api.api.controller diff --git a/src/test/java/com/_1/spring_rest_api/service/CourseServiceTest.java b/src/test/java/com/_1/spring_rest_api/service/CourseServiceTest.java index 27c1a5f..885269f 100644 --- a/src/test/java/com/_1/spring_rest_api/service/CourseServiceTest.java +++ b/src/test/java/com/_1/spring_rest_api/service/CourseServiceTest.java @@ -11,6 +11,7 @@ import com._1.spring_rest_api.entity.Week; import com._1.spring_rest_api.repository.CourseRepository; +import com._1.spring_rest_api.repository.UserRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -32,6 +33,9 @@ class CourseServiceTest { @Mock private CourseRepository courseRepository; + @Mock + private UserRepository userRepository; + @InjectMocks private CourseService courseService; @@ -41,7 +45,13 @@ class CourseServiceTest { @BeforeEach void setUp() { - testUser = new User(1L); + testUser = User.builder() + .id(1L) + .email("test@example.com") + .name("Test User") + .isActive(true) + .build(); + testCourse = new Course(1L, testUser, "Test Course", "Test Description"); testWeeks = new ArrayList<>(); @@ -59,9 +69,11 @@ void setUp() { @DisplayName("코스 생성 성공 테스트") void createCourse_Success() { // Given + when(userRepository.findById(1L)).thenReturn(Optional.of(testUser)); + when(courseRepository.save(any(Course.class))).thenAnswer(invocation -> { Course inputCourse = invocation.getArgument(0); - ReflectionTestUtils.setField(inputCourse, "id", 1L); // ID 설정 + ReflectionTestUtils.setField(inputCourse, "id", 1L); return inputCourse; }); diff --git a/src/test/java/com/_1/spring_rest_api/service/UserServiceTest.java b/src/test/java/com/_1/spring_rest_api/service/UserServiceTest.java index 13cbc09..c8646cc 100644 --- a/src/test/java/com/_1/spring_rest_api/service/UserServiceTest.java +++ b/src/test/java/com/_1/spring_rest_api/service/UserServiceTest.java @@ -173,7 +173,7 @@ private UserKakao createTestUserKakao(User user) { .build(); UserKakao saved = userKakaoRepository.save(userKakao); - userKakao.linkWithUser(user); + userKakao.changeUser(user); userRepository.save(user);