diff --git a/backend/src/main/java/wooteco/prolog/roadmap/application/KeywordService.java b/backend/src/main/java/wooteco/prolog/roadmap/application/KeywordService.java index b7dd4b696..b76335c4c 100644 --- a/backend/src/main/java/wooteco/prolog/roadmap/application/KeywordService.java +++ b/backend/src/main/java/wooteco/prolog/roadmap/application/KeywordService.java @@ -1,9 +1,5 @@ package wooteco.prolog.roadmap.application; -import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_KEYWORD_NOT_FOUND_EXCEPTION; -import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_SESSION_NOT_FOUND_EXCEPTION; - -import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import wooteco.prolog.common.exception.BadRequestException; @@ -15,6 +11,12 @@ import wooteco.prolog.roadmap.domain.repository.KeywordRepository; import wooteco.prolog.session.domain.repository.SessionRepository; +import java.util.List; + +import static java.util.Collections.emptyMap; +import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_KEYWORD_NOT_FOUND_EXCEPTION; +import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_SESSION_NOT_FOUND_EXCEPTION; + @Transactional @Service public class KeywordService { @@ -23,7 +25,7 @@ public class KeywordService { private final KeywordRepository keywordRepository; public KeywordService(final SessionRepository sessionRepository, - final KeywordRepository keywordRepository) { + final KeywordRepository keywordRepository) { this.sessionRepository = sessionRepository; this.keywordRepository = keywordRepository; } @@ -65,7 +67,7 @@ public KeywordResponse findKeywordWithAllChild(final Long sessionId, final Long Keyword keyword = keywordRepository.findFetchByIdOrderBySeq(keywordId); - return KeywordResponse.createWithAllChildResponse(keyword); + return KeywordResponse.createWithAllChildResponse(keyword, emptyMap(), emptyMap()); } @Transactional(readOnly = true) @@ -74,7 +76,7 @@ public KeywordResponse newFindKeywordWithAllChild(final Long keywordId) { Keyword keyword = keywordRepository.findFetchByIdOrderBySeq(keywordId); - return KeywordResponse.createWithAllChildResponse(keyword); + return KeywordResponse.createWithAllChildResponse(keyword, emptyMap(), emptyMap()); } @Transactional(readOnly = true) @@ -83,14 +85,14 @@ public KeywordsResponse findSessionIncludeRootKeywords(final Long sessionId) { List keywords = keywordRepository.findBySessionIdAndParentIsNull(sessionId); - return KeywordsResponse.createResponse(keywords); + return KeywordsResponse.of(keywords, emptyMap(), emptyMap()); } @Transactional(readOnly = true) public KeywordsResponse newFindSessionIncludeRootKeywords() { List keywords = keywordRepository.newFindByParentIsNull(); - return KeywordsResponse.createResponse(keywords); + return KeywordsResponse.of(keywords, emptyMap(), emptyMap()); } public void updateKeyword( diff --git a/backend/src/main/java/wooteco/prolog/roadmap/application/RoadMapService.java b/backend/src/main/java/wooteco/prolog/roadmap/application/RoadMapService.java index 96215d86e..bff726073 100644 --- a/backend/src/main/java/wooteco/prolog/roadmap/application/RoadMapService.java +++ b/backend/src/main/java/wooteco/prolog/roadmap/application/RoadMapService.java @@ -1,122 +1,47 @@ package wooteco.prolog.roadmap.application; -import java.util.Comparator; -import java.util.HashSet; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import wooteco.prolog.common.exception.BadRequestException; -import wooteco.prolog.roadmap.application.dto.KeywordResponse; import wooteco.prolog.roadmap.application.dto.KeywordsResponse; -import wooteco.prolog.roadmap.application.dto.RecommendedPostResponse; -import wooteco.prolog.roadmap.domain.Curriculum; -import wooteco.prolog.roadmap.domain.EssayAnswer; import wooteco.prolog.roadmap.domain.Keyword; -import wooteco.prolog.roadmap.domain.Quiz; -import wooteco.prolog.roadmap.domain.repository.CurriculumRepository; -import wooteco.prolog.roadmap.domain.repository.EssayAnswerRepository; import wooteco.prolog.roadmap.domain.repository.KeywordRepository; -import wooteco.prolog.roadmap.domain.repository.QuizRepository; -import wooteco.prolog.session.domain.Session; -import wooteco.prolog.session.domain.repository.SessionRepository; +import wooteco.prolog.roadmap.domain.repository.dto.KeywordIdAndAnsweredQuizCount; +import wooteco.prolog.roadmap.domain.repository.dto.KeywordIdAndTotalQuizCount; import java.util.List; import java.util.Map; -import java.util.Set; -import static java.util.stream.Collectors.groupingBy; -import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toSet; -import static wooteco.prolog.common.exception.BadRequestCode.CURRICULUM_NOT_FOUND_EXCEPTION; +import static java.util.stream.Collectors.toMap; @RequiredArgsConstructor -@Transactional +@Transactional(readOnly = true) @Service public class RoadMapService { - private final CurriculumRepository curriculumRepository; - private final SessionRepository sessionRepository; private final KeywordRepository keywordRepository; - private final QuizRepository quizRepository; - private final EssayAnswerRepository essayAnswerRepository; - @Transactional(readOnly = true) public KeywordsResponse findAllKeywordsWithProgress(final Long curriculumId, final Long memberId) { - final Curriculum curriculum = curriculumRepository.findById(curriculumId) - .orElseThrow(() -> new BadRequestException(CURRICULUM_NOT_FOUND_EXCEPTION)); + final List keywords = keywordRepository.findAllByCurriculumId(curriculumId); + final Map totalQuizCounts = getTotalQuizCounts(); + final Map answeredQuizCounts = getAnsweredQuizCounts(memberId); - final List keywordsInCurriculum = getKeywordsInCurriculum(curriculum); - - final Map> quizzesInKeywords = quizRepository.findAll().stream() - .collect(groupingBy(Quiz::getKeyword, toSet())); - - return createResponsesWithProgress(keywordsInCurriculum, quizzesInKeywords, getDoneQuizzes(memberId)); - } - - private Set getDoneQuizzes(final Long memberId) { - return essayAnswerRepository.findAllByMemberId(memberId).stream() - .map(EssayAnswer::getQuiz) - .collect(toSet()); - } - - private List getKeywordsInCurriculum(final Curriculum curriculum) { - final Set sessionIds = sessionRepository.findAllByCurriculumId(curriculum.getId()) - .stream() - .map(Session::getId) - .collect(toSet()); - - return keywordRepository.findBySessionIdIn(sessionIds); - } - - private KeywordsResponse createResponsesWithProgress(final List keywords, - final Map> quizzesPerKeyword, - final Set doneQuizzes) { - final List keywordResponses = keywords.stream() - .filter(Keyword::isRoot) - .map(keyword -> createResponseWithProgress(keyword, quizzesPerKeyword, doneQuizzes)) - .sorted(Comparator.comparing(KeywordResponse::getKeywordId)) - .collect(toList()); - - return new KeywordsResponse(keywordResponses); - } - - private KeywordResponse createResponseWithProgress(final Keyword keyword, - final Map> quizzesPerKeyword, - final Set doneQuizzes) { - final int totalQuizCount = quizzesPerKeyword.getOrDefault(keyword, new HashSet<>()).size(); - final int doneQuizCount = getDoneQuizCount( - quizzesPerKeyword.getOrDefault(keyword, new HashSet<>()), doneQuizzes); - - final List recommendedPostResponses = keyword.getRecommendedPosts().stream() - .map(RecommendedPostResponse::from) - .collect(toList()); - - return new KeywordResponse( - keyword.getId(), - keyword.getName(), - keyword.getDescription(), - keyword.getSeq(), - keyword.getImportance(), - totalQuizCount, - doneQuizCount, - keyword.getParentIdOrNull(), - recommendedPostResponses, - createChildrenWithProgress(keyword.getChildren(), quizzesPerKeyword, doneQuizzes) - ); + return KeywordsResponse.of(keywords, totalQuizCounts, answeredQuizCounts); } - private int getDoneQuizCount(final Set quizzes, final Set doneQuizzes) { - quizzes.retainAll(doneQuizzes); - return quizzes.size(); + private Map getTotalQuizCounts() { + return keywordRepository.findTotalQuizCount().stream() + .collect( + toMap( + KeywordIdAndTotalQuizCount::getKeywordId, + KeywordIdAndTotalQuizCount::getTotalQuizCount)); } - private List createChildrenWithProgress(final Set children, - final Map> quizzesPerKeyword, - final Set userAnswers) { - return children.stream() - .map(child -> createResponseWithProgress(child, quizzesPerKeyword, userAnswers)) - .sorted(Comparator.comparing(KeywordResponse::getKeywordId)) - .collect(Collectors.toList()); + private Map getAnsweredQuizCounts(final Long memberId) { + return keywordRepository.findAnsweredQuizCountByMemberId(memberId).stream() + .collect( + toMap( + KeywordIdAndAnsweredQuizCount::getKeywordId, + KeywordIdAndAnsweredQuizCount::getAnsweredQuizCount)); } } diff --git a/backend/src/main/java/wooteco/prolog/roadmap/application/dto/KeywordResponse.java b/backend/src/main/java/wooteco/prolog/roadmap/application/dto/KeywordResponse.java index aa94ea5d2..683bf3e49 100644 --- a/backend/src/main/java/wooteco/prolog/roadmap/application/dto/KeywordResponse.java +++ b/backend/src/main/java/wooteco/prolog/roadmap/application/dto/KeywordResponse.java @@ -1,13 +1,13 @@ package wooteco.prolog.roadmap.application.dto; -import java.util.ArrayList; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import wooteco.prolog.roadmap.domain.Keyword; -import java.util.HashSet; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -21,14 +21,14 @@ public class KeywordResponse { private int order; private int importance; private int totalQuizCount; - private int doneQuizCount; + private int answeredQuizCount; private Long parentKeywordId; private List recommendedPosts; private List childrenKeywords; public KeywordResponse(final Long keywordId, final String name, final String description, final int order, final int importance, final int totalQuizCount, - final int doneQuizCount, final Long parentKeywordId, + final int answeredQuizCount, final Long parentKeywordId, final List recommendedPosts, final List childrenKeywords) { this.keywordId = keywordId; @@ -37,7 +37,7 @@ public KeywordResponse(final Long keywordId, final String name, final String des this.order = order; this.importance = importance; this.totalQuizCount = totalQuizCount; - this.doneQuizCount = doneQuizCount; + this.answeredQuizCount = answeredQuizCount; this.parentKeywordId = parentKeywordId; this.recommendedPosts = recommendedPosts; this.childrenKeywords = childrenKeywords; @@ -53,34 +53,36 @@ public static KeywordResponse createResponse(final Keyword keyword) { 0, 0, keyword.getParentIdOrNull(), createRecommendedPostResponses(keyword), - null); + Collections.emptyList()); } - private static List createRecommendedPostResponses(final Keyword keyword) { - return keyword.getRecommendedPosts().stream() - .map(RecommendedPostResponse::from) - .collect(Collectors.toList()); - } - - public static KeywordResponse createWithAllChildResponse(final Keyword keyword) { + public static KeywordResponse createWithAllChildResponse(final Keyword keyword, + final Map totalQuizCounts, + final Map answeredQuizCounts) { return new KeywordResponse( keyword.getId(), keyword.getName(), keyword.getDescription(), keyword.getSeq(), keyword.getImportance(), - 0, - 0, + totalQuizCounts.getOrDefault(keyword.getId(), 0), + answeredQuizCounts.getOrDefault(keyword.getId(), 0), keyword.getParentIdOrNull(), createRecommendedPostResponses(keyword), - createChildren(keyword.getChildren())); + createChildren(keyword.getChildren(), totalQuizCounts, answeredQuizCounts)); } - private static List createChildren(final Set children) { - List keywords = new ArrayList<>(); - for (Keyword keyword : children) { - keywords.add(createWithAllChildResponse(keyword)); - } - return keywords; + private static List createRecommendedPostResponses(final Keyword keyword) { + return keyword.getRecommendedPosts().stream() + .map(RecommendedPostResponse::from) + .collect(Collectors.toList()); + } + + private static List createChildren(final Set children, + final Map totalQuizCounts, + final Map answeredQuizCounts) { + return children.stream() + .map(keyword -> createWithAllChildResponse(keyword, totalQuizCounts, answeredQuizCounts)) + .collect(Collectors.toList()); } } diff --git a/backend/src/main/java/wooteco/prolog/roadmap/application/dto/KeywordsResponse.java b/backend/src/main/java/wooteco/prolog/roadmap/application/dto/KeywordsResponse.java index d904b67a3..c5927f0a1 100644 --- a/backend/src/main/java/wooteco/prolog/roadmap/application/dto/KeywordsResponse.java +++ b/backend/src/main/java/wooteco/prolog/roadmap/application/dto/KeywordsResponse.java @@ -1,12 +1,14 @@ package wooteco.prolog.roadmap.application.dto; -import java.util.List; -import java.util.stream.Collectors; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import wooteco.prolog.roadmap.domain.Keyword; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter public class KeywordsResponse { @@ -17,18 +19,14 @@ public KeywordsResponse(final List data) { this.data = data; } - public static KeywordsResponse createResponse(final List keywords) { - List keywordsResponse = keywords.stream() - .map(KeywordResponse::createResponse) - .collect(Collectors.toList()); - return new KeywordsResponse(keywordsResponse); - } - - public static KeywordsResponse createResponseWithChildren(final List keywords) { - List keywordsResponse = keywords.stream() + public static KeywordsResponse of(final List keywords, + final Map totalQuizCounts, + final Map answeredQuizCounts) { + final List keywordsResponse = keywords.stream() .filter(Keyword::isRoot) - .map(KeywordResponse::createWithAllChildResponse) + .map(rootKeyword -> KeywordResponse.createWithAllChildResponse(rootKeyword, totalQuizCounts, answeredQuizCounts)) .collect(Collectors.toList()); + return new KeywordsResponse(keywordsResponse); } } diff --git a/backend/src/main/java/wooteco/prolog/roadmap/domain/Keyword.java b/backend/src/main/java/wooteco/prolog/roadmap/domain/Keyword.java index 9fc136b14..6259312d2 100644 --- a/backend/src/main/java/wooteco/prolog/roadmap/domain/Keyword.java +++ b/backend/src/main/java/wooteco/prolog/roadmap/domain/Keyword.java @@ -7,7 +7,16 @@ import org.hibernate.annotations.BatchSize; import wooteco.prolog.common.exception.BadRequestException; -import javax.persistence.*; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; import java.util.HashSet; import java.util.Objects; import java.util.Set; @@ -38,6 +47,7 @@ public class Keyword { @Column(name = "session_id", nullable = false) private Long sessionId; + @BatchSize(size = 1000) @OneToMany(mappedBy = "keyword", cascade = CascadeType.ALL, orphanRemoval = true) private Set recommendedPosts = new HashSet<>(); @@ -50,8 +60,7 @@ public class Keyword { private Set children = new HashSet<>(); public Keyword(final Long id, final String name, final String description, final int seq, - final int importance, - final Long sessionId, final Keyword parent, final Set children) { + final int importance, final Long sessionId, final Keyword parent, final Set children) { validateSeq(seq); this.id = id; this.name = name; @@ -69,7 +78,7 @@ public static Keyword createKeyword(final String name, final int importance, final Long sessionId, final Keyword parent) { - return new Keyword(null, name, description, seq, importance, sessionId, parent, null); + return new Keyword(null, name, description, seq, importance, sessionId, parent, new HashSet<>()); } public void update(final String name, final String description, final int seq, diff --git a/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/KeywordRepository.java b/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/KeywordRepository.java index db4207efe..c9f96348b 100644 --- a/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/KeywordRepository.java +++ b/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/KeywordRepository.java @@ -1,35 +1,30 @@ package wooteco.prolog.roadmap.domain.repository; -import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import wooteco.prolog.roadmap.domain.Keyword; +import wooteco.prolog.roadmap.domain.repository.dto.KeywordIdAndAnsweredQuizCount; +import wooteco.prolog.roadmap.domain.repository.dto.KeywordIdAndTotalQuizCount; import java.util.List; -import java.util.Optional; -import java.util.Set; - -import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.FETCH; public interface KeywordRepository extends JpaRepository { - @EntityGraph(attributePaths = "recommendedPosts", type = FETCH) - Optional findById(final Long id); + @Query("SELECT k.id AS keywordId, COUNT (q.id) as totalQuizCount " + + "FROM Keyword k " + + "JOIN Quiz q ON q.keyword.id = k.id " + + "GROUP BY k.id") + List findTotalQuizCount(); - @EntityGraph(attributePaths = "recommendedPosts", type = FETCH) - List findAll(); + @Query("SELECT k.id AS keywordId, COUNT (e.id) AS answeredQuizCount " + + "FROM Keyword k " + + "JOIN Quiz q ON k.id = q.keyword.id " + + "JOIN EssayAnswer e ON e.quiz.id = q.id " + + "WHERE e.member.id = :memberId " + + "GROUP BY k.id ") + List findAnsweredQuizCountByMemberId(@Param("memberId") Long memberId); - @Query("SELECT k FROM Keyword k " - + "LEFT JOIN FETCH k.children c " - + "LEFT JOIN FETCH k.recommendedPosts " - + "LEFT JOIN FETCH k.parent p " - + "LEFT JOIN FETCH p.recommendedPosts " - + "LEFT JOIN FETCH c.recommendedPosts " - + "LEFT JOIN FETCH c.children lc " - + "LEFT JOIN FETCH lc.recommendedPosts " - + "LEFT JOIN FETCH lc.children " - + "WHERE k.id = :keywordId ORDER BY k.seq") Keyword findFetchByIdOrderBySeq(@Param("keywordId") Long keywordId); @Query("SELECT k FROM Keyword k " @@ -42,5 +37,8 @@ public interface KeywordRepository extends JpaRepository { + "WHERE k.parent IS NULL") List newFindByParentIsNull(); - List findBySessionIdIn(final Set sessionIds); + @Query("SELECT k FROM Keyword k " + + "JOIN Session s ON s.id = k.sessionId " + + "WHERE s.curriculumId = :curriculumId ") + List findAllByCurriculumId(Long curriculumId); } diff --git a/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/QuizRepository.java b/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/QuizRepository.java index 6ecdf58a1..944abf921 100644 --- a/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/QuizRepository.java +++ b/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/QuizRepository.java @@ -1,10 +1,11 @@ package wooteco.prolog.roadmap.domain.repository; -import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import wooteco.prolog.roadmap.domain.Quiz; +import java.util.List; + public interface QuizRepository extends JpaRepository { @Query("SELECT q FROM Quiz q" diff --git a/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/dto/KeywordIdAndAnsweredQuizCount.java b/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/dto/KeywordIdAndAnsweredQuizCount.java new file mode 100644 index 000000000..63009ab0e --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/dto/KeywordIdAndAnsweredQuizCount.java @@ -0,0 +1,7 @@ +package wooteco.prolog.roadmap.domain.repository.dto; + +public interface KeywordIdAndAnsweredQuizCount { + long getKeywordId(); + + int getAnsweredQuizCount(); +} diff --git a/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/dto/KeywordIdAndTotalQuizCount.java b/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/dto/KeywordIdAndTotalQuizCount.java new file mode 100644 index 000000000..47836c550 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/dto/KeywordIdAndTotalQuizCount.java @@ -0,0 +1,7 @@ +package wooteco.prolog.roadmap.domain.repository.dto; + +public interface KeywordIdAndTotalQuizCount { + long getKeywordId(); + + int getTotalQuizCount(); +} diff --git a/backend/src/test/java/wooteco/prolog/roadmap/application/RecommendedPostServiceTest.java b/backend/src/test/java/wooteco/prolog/roadmap/application/RecommendedPostServiceTest.java index 5f3932c58..1a5417144 100644 --- a/backend/src/test/java/wooteco/prolog/roadmap/application/RecommendedPostServiceTest.java +++ b/backend/src/test/java/wooteco/prolog/roadmap/application/RecommendedPostServiceTest.java @@ -17,7 +17,6 @@ import wooteco.prolog.session.domain.repository.SessionRepository; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.SoftAssertions.assertSoftly; @SpringBootTest class RecommendedPostServiceTest { @@ -90,10 +89,6 @@ void delete() { recommendedPostService.delete(recommendedPostId); //then - assertSoftly(softAssertions -> { - assertThat(recommendedPostRepository.findAll()).hasSize(0); - assertThat(keywordRepository.findById(keyword.getId()).get().getRecommendedPosts()) - .isEmpty(); - }); + assertThat(recommendedPostRepository.existsById(recommendedPostId)).isFalse(); } } diff --git a/backend/src/test/java/wooteco/prolog/roadmap/application/RoadMapServiceTest.java b/backend/src/test/java/wooteco/prolog/roadmap/application/RoadMapServiceTest.java index 45d584adb..fac37a2ff 100644 --- a/backend/src/test/java/wooteco/prolog/roadmap/application/RoadMapServiceTest.java +++ b/backend/src/test/java/wooteco/prolog/roadmap/application/RoadMapServiceTest.java @@ -1,92 +1,202 @@ package wooteco.prolog.roadmap.application; +import org.assertj.core.api.AbstractListAssert; +import org.assertj.core.api.ObjectAssert; +import org.assertj.core.api.ProxyableListAssert; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import wooteco.prolog.common.DataInitializer; import wooteco.prolog.member.domain.Member; import wooteco.prolog.member.domain.Role; +import wooteco.prolog.member.domain.repository.MemberRepository; import wooteco.prolog.roadmap.application.dto.KeywordResponse; import wooteco.prolog.roadmap.application.dto.KeywordsResponse; -import wooteco.prolog.roadmap.domain.Curriculum; import wooteco.prolog.roadmap.domain.EssayAnswer; import wooteco.prolog.roadmap.domain.Keyword; import wooteco.prolog.roadmap.domain.Quiz; -import wooteco.prolog.roadmap.domain.repository.CurriculumRepository; import wooteco.prolog.roadmap.domain.repository.EssayAnswerRepository; import wooteco.prolog.roadmap.domain.repository.KeywordRepository; import wooteco.prolog.roadmap.domain.repository.QuizRepository; import wooteco.prolog.session.domain.Session; import wooteco.prolog.session.domain.repository.SessionRepository; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Optional; +import java.util.function.Consumer; +import static java.util.Collections.emptySet; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.when; -@ExtendWith(MockitoExtension.class) +@SpringBootTest class RoadMapServiceTest { - @Mock - private CurriculumRepository curriculumRepository; - @Mock + @Autowired private SessionRepository sessionRepository; - @Mock + @Autowired private KeywordRepository keywordRepository; - @Mock + @Autowired private QuizRepository quizRepository; - @Mock + @Autowired private EssayAnswerRepository essayAnswerRepository; - @InjectMocks + @Autowired + private MemberRepository memberRepository; + + @Autowired private RoadMapService roadMapService; + @Autowired + private DataInitializer dataInitializer; + + @AfterEach + void truncate() { + dataInitializer.execute(); + } + @Test - @DisplayName("curriculumId가 주어지면 해당 커리큘럼의 키워드들을 학습 진도와 함께 전부 조회할 수 있다.") + @DisplayName("커리큘럼 ID에 해당하는 모든 키워드를 진도율과 함께 조회할 수 있다 - 비회원") void findAllKeywordsWithProgress() { //given - final Curriculum curriculum = new Curriculum(1L, "커리큘럼1"); - final Session session = new Session(1L, curriculum.getId(), "세션1"); - final List sessions = Arrays.asList(session); - final Keyword keyword = new Keyword(1L, "자바1", "자바 설명1", 1, 5, session.getId(), - null, Collections.emptySet()); - final Quiz quiz = new Quiz(1L, keyword, "자바8을 왜 쓰나요?"); - final Member member = new Member(1L, "연어", "참치", Role.CREW, 1L, "image"); - final EssayAnswer essayAnswer = new EssayAnswer(quiz, "쓰라고 해서요ㅠㅠ", member); + final Long curriculumId = 1L; + final Long session1 = createSession(curriculumId); + final Long session2 = createSession(curriculumId); + + final Keyword root1 = createParentKeyword("스트림", "스트림 API 설명", session1); + final Keyword root2 = createParentKeyword("컬렉션", "컬렉션 API 설명", session2); + + final Keyword oneDepthChild = createChildKeyword(root1, "map()", "스트림 api의 map"); + final Keyword twoDepthChild = createChildKeyword(oneDepthChild, "map()의 파라미터", "파라미터 설명"); + + createQuiz(oneDepthChild, "map을 왜 쓰나요?"); + createQuiz(root2, "컬렉션을 왜 쓰나요?"); + + //when + final KeywordsResponse response = roadMapService.findAllKeywordsWithProgress(curriculumId, null); + + //then + assertSoftly(softAssertions -> { + final ProxyableListAssert roots = softAssertions.assertThat(response.getData()); + final AbstractListAssert, KeywordResponse, ObjectAssert> oneDepthChildren + = roots.flatMap(KeywordResponse::getChildrenKeywords); + final AbstractListAssert, KeywordResponse, ObjectAssert> twoDepthChildren + = oneDepthChildren.flatMap(KeywordResponse::getChildrenKeywords); + + roots.hasSize(2) + .satisfies(containsKeywordIds(root1.getId(), root2.getId())) + .satisfies(containsTotalQuizCounts(0, 1)) + .satisfies(containsAnsweredQuizCounts(0, 0)); + + oneDepthChildren.hasSize(1) + .satisfies(containsKeywordIds(oneDepthChild.getId())) + .satisfies(containsTotalQuizCounts(1)) + .satisfies(containsAnsweredQuizCounts(0)); + + twoDepthChildren.hasSize(1) + .satisfies(containsKeywordIds(twoDepthChild.getId())) + .satisfies(containsTotalQuizCounts(0)) + .satisfies(containsAnsweredQuizCounts(0)); + }); + } + + @Test + @DisplayName("커리큘럼 ID에 해당하는 모든 키워드를 진도율과 함께 조회할 수 있다 - 회원") + void findAllKeywordsWithProgress_login() { + //given + final Long curriculumId = 1L; + final Long session1 = createSession(curriculumId); + final Long session2 = createSession(curriculumId); - when(curriculumRepository.findById(anyLong())) - .thenReturn(Optional.of(curriculum)); + final Keyword root1 = createParentKeyword("스트림", "스트림 API 설명", session1); + final Keyword root2 = createParentKeyword("컬렉션", "컬렉션 API 설명", session2); - when(sessionRepository.findAllByCurriculumId(anyLong())) - .thenReturn(sessions); + final Keyword oneDepthChild = createChildKeyword(root1, "map()", "스트림 api의 map"); + final Keyword twoDepthChild = createChildKeyword(oneDepthChild, "map()의 파라미터", "파라미터 설명"); - when(keywordRepository.findBySessionIdIn(any())) - .thenReturn(Arrays.asList(keyword)); + final Quiz quiz1 = createQuiz(root2, "컬렉션을 왜 쓰나요?"); + final Quiz quiz2 = createQuiz(twoDepthChild, "파라미터의 종류는?"); + createQuiz(oneDepthChild, "map을 왜 쓰나요?"); - when(essayAnswerRepository.findAllByMemberId(1L)) - .thenReturn(new HashSet<>(Arrays.asList(essayAnswer))); + final Member member = createMember(); - when(quizRepository.findAll()) - .thenReturn(Arrays.asList(quiz)); + createEssayAnswer(quiz1, member, "배열을 쓸 수는 없으니까요"); + createEssayAnswer(quiz2, member, "Function 입니다"); //when - final KeywordsResponse actual = - roadMapService.findAllKeywordsWithProgress(curriculum.getId(), 1L); + final KeywordsResponse response = roadMapService.findAllKeywordsWithProgress(curriculumId, member.getId()); //then - final List responses = actual.getData(); - assertSoftly(soft -> { - assertThat(responses).hasSize(1); - assertThat(responses.get(0).getDoneQuizCount()).isOne(); - assertThat(responses.get(0).getTotalQuizCount()).isOne(); + assertSoftly(softAssertions -> { + final ProxyableListAssert roots = softAssertions.assertThat(response.getData()); + final AbstractListAssert, KeywordResponse, ObjectAssert> oneDepthChildren + = roots.flatMap(KeywordResponse::getChildrenKeywords); + final AbstractListAssert, KeywordResponse, ObjectAssert> twoDepthChildren + = oneDepthChildren.flatMap(KeywordResponse::getChildrenKeywords); + + roots.hasSize(2) + .satisfies(containsKeywordIds(root1.getId(), root2.getId())) + .satisfies(containsTotalQuizCounts(0, 1)) + .satisfies(containsAnsweredQuizCounts(0, 1)); + + oneDepthChildren.hasSize(1) + .satisfies(containsKeywordIds(oneDepthChild.getId())) + .satisfies(containsTotalQuizCounts(1)) + .satisfies(containsAnsweredQuizCounts(0)); + + twoDepthChildren.hasSize(1) + .satisfies(containsKeywordIds(twoDepthChild.getId())) + .satisfies(containsTotalQuizCounts(1)) + .satisfies(containsAnsweredQuizCounts(1)); }); } + + private Consumer> containsAnsweredQuizCounts(final Integer... expected) { + return keywords -> assertThat(keywords) + .map(KeywordResponse::getAnsweredQuizCount) + .containsExactlyInAnyOrder(expected); + } + + private Consumer> containsTotalQuizCounts(final Integer... expected) { + return keywords -> assertThat(keywords) + .map(KeywordResponse::getTotalQuizCount) + .containsExactlyInAnyOrder(expected); + } + + private Consumer> containsKeywordIds(final Long... expected) { + return keywords -> assertThat(keywords) + .map(KeywordResponse::getKeywordId) + .containsExactlyInAnyOrder(expected); + } + + private Long createSession(final Long curriculumId) { + Session session = new Session(curriculumId, "테스트 세션"); + sessionRepository.save(session); + return session.getId(); + } + + private Keyword createParentKeyword(final String name, final String description, final Long sessionId) { + final Keyword keyword = new Keyword(null, name, description, 1, 1, sessionId, null, emptySet()); + return keywordRepository.save(keyword); + } + + private Keyword createChildKeyword(final Keyword parent, final String name, final String description) { + final Keyword keyword = new Keyword(null, name, description, 1, 1, parent.getSessionId(), parent, emptySet()); + return keywordRepository.save(keyword); + } + + private Quiz createQuiz(final Keyword keyword, final String question) { + final Quiz quiz = new Quiz(keyword, question); + return quizRepository.save(quiz); + } + + private Member createMember() { + final Member member = new Member("id", "연어", Role.CREW, 1L, "image"); + return memberRepository.save(member); + } + + private EssayAnswer createEssayAnswer(final Quiz quiz, final Member member, final String answer) { + final EssayAnswer essayAnswer = new EssayAnswer(quiz, answer, member); + return essayAnswerRepository.save(essayAnswer); + } } diff --git a/backend/src/test/java/wooteco/prolog/roadmap/repository/KeywordRepositoryTest.java b/backend/src/test/java/wooteco/prolog/roadmap/repository/KeywordRepositoryTest.java index 47c67b86b..4f018ebce 100644 --- a/backend/src/test/java/wooteco/prolog/roadmap/repository/KeywordRepositoryTest.java +++ b/backend/src/test/java/wooteco/prolog/roadmap/repository/KeywordRepositoryTest.java @@ -1,23 +1,33 @@ package wooteco.prolog.roadmap.repository; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import javax.persistence.EntityManager; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import wooteco.prolog.member.domain.Member; +import wooteco.prolog.member.domain.Role; +import wooteco.prolog.member.domain.repository.MemberRepository; import wooteco.prolog.roadmap.domain.Curriculum; +import wooteco.prolog.roadmap.domain.EssayAnswer; import wooteco.prolog.roadmap.domain.Keyword; +import wooteco.prolog.roadmap.domain.Quiz; import wooteco.prolog.roadmap.domain.repository.CurriculumRepository; +import wooteco.prolog.roadmap.domain.repository.EssayAnswerRepository; import wooteco.prolog.roadmap.domain.repository.KeywordRepository; +import wooteco.prolog.roadmap.domain.repository.QuizRepository; +import wooteco.prolog.roadmap.domain.repository.dto.KeywordIdAndAnsweredQuizCount; +import wooteco.prolog.roadmap.domain.repository.dto.KeywordIdAndTotalQuizCount; import wooteco.prolog.session.domain.Session; import wooteco.prolog.session.domain.repository.SessionRepository; import wooteco.support.utils.RepositoryTest; +import javax.persistence.EntityManager; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.junit.jupiter.api.Assertions.assertAll; + @RepositoryTest class KeywordRepositoryTest { @@ -26,16 +36,20 @@ class KeywordRepositoryTest { @Autowired private SessionRepository sessionRepository; @Autowired + private EssayAnswerRepository essayAnswerRepository; + @Autowired + private MemberRepository memberRepository; + @Autowired private EntityManager em; @Autowired private CurriculumRepository curriculumRepository; + @Autowired + private QuizRepository quizRepository; @Test void 부모_키워드와_1뎁스까지의_자식_키워드를_함께_조회할_수_있다() { // given - Session session = new Session("테스트 세션"); - sessionRepository.save(session); - Long sessionId = session.getId(); + Long sessionId = createSession(); Keyword keywordParent = createKeywordParent( Keyword.createKeyword("자바", "자바에 대한 설명", 1, 1, sessionId, null)); @@ -58,9 +72,7 @@ class KeywordRepositoryTest { @Test void 부모_키워드와_2뎁스까지의_자식_키워드를_함께_조회할_수_있다() { // given - Session session = new Session("테스트 세션"); - sessionRepository.save(session); - Long sessionId = session.getId(); + Long sessionId = createSession(); Keyword parent = createKeywordParent( Keyword.createKeyword("자바", "자바에 대한 설명", 1, 1, sessionId, null)); @@ -93,9 +105,7 @@ class KeywordRepositoryTest { @Test void 최상위_키워드만_조회한다() { // given - Session session = new Session("테스트 세션"); - sessionRepository.save(session); - Long sessionId = session.getId(); + Long sessionId = createSession(); Keyword keywordParent = createKeywordParent( Keyword.createKeyword("자바", "자바에 대한 설명", 1, 1, sessionId, null)); @@ -111,12 +121,12 @@ class KeywordRepositoryTest { sessionId); // then - assertThat(extractParentKeyword.size()).isEqualTo(1); + assertThat(extractParentKeyword).hasSize(1); } @Test - @DisplayName("세션 ID 리스트로 키워드 리스트를 조회한다") - void findBySessionIdIn() { + @DisplayName("newFindParentIsNull() : 모든 상위 키워드들을 조회할 수 있다.") + void newFindParentIsNull() { //given final Curriculum curriculum = curriculumRepository.save(new Curriculum("커리큘럼1")); @@ -134,74 +144,128 @@ void findBySessionIdIn() { Keyword.createKeyword("자바3", "자바 설명3", 3, 5, session1.getId(), null)); final Keyword keyword4 = keywordRepository.save( Keyword.createKeyword("자바4", "자바 설명4", 4, 5, session1.getId(), keyword3)); - keywordRepository.save( + final Keyword keyword5 = keywordRepository.save( Keyword.createKeyword("자바5", "자바 설명5", 5, 5, session2.getId(), null)); - keywordRepository.save( + final Keyword keyword6 = keywordRepository.save( Keyword.createKeyword("자바6", "자바 설명6", 6, 5, session2.getId(), keyword1)); - keywordRepository.save( + final Keyword keyword7 = keywordRepository.save( Keyword.createKeyword("자바7", "자바 설명7", 7, 5, session2.getId(), null)); - keywordRepository.save( + final Keyword keyword8 = keywordRepository.save( Keyword.createKeyword("자바8", "자바 설명8", 8, 5, session3.getId(), keyword2)); final Keyword keyword9 = keywordRepository.save( Keyword.createKeyword("자바9", "자바 설명9", 9, 5, session4.getId(), keyword2)); final Keyword keyword10 = keywordRepository.save( Keyword.createKeyword("자바10", "자바 설명10", 10, 5, session5.getId(), null)); - final HashSet sessionIds = new HashSet<>( - Arrays.asList(session1.getId(), session4.getId(), session5.getId()) - ); - //when - final List keywords = keywordRepository.findBySessionIdIn(sessionIds); + final List keywords = keywordRepository.newFindByParentIsNull(); //then assertThat(keywords) .usingRecursiveComparison() .ignoringFields("id", "parent.id") - .isEqualTo(Arrays.asList(keyword1, keyword2, keyword3, keyword4, keyword9, keyword10)); + .isEqualTo(Arrays.asList(keyword1, keyword3, keyword5, keyword7, keyword10)); } @Test - @DisplayName("newFindParentIsNull() : 모든 상위 키워드들을 조회할 수 있다.") - void newFindParentIsNull() { + @DisplayName("각 키워드별 퀴즈 개수를 조회할 수 있다") + void findTotalQuizCount() { //given - final Curriculum curriculum = curriculumRepository.save(new Curriculum("커리큘럼1")); + final Long sessionId = createSession(); - final Session session1 = sessionRepository.save(new Session(curriculum.getId(), "세션1")); - final Session session2 = sessionRepository.save(new Session(curriculum.getId(), "세션2")); - final Session session3 = sessionRepository.save(new Session(curriculum.getId(), "세션3")); - final Session session4 = sessionRepository.save(new Session(curriculum.getId(), "세션4")); - final Session session5 = sessionRepository.save(new Session(curriculum.getId(), "세션5")); + final Keyword parent = createKeywordParent( + Keyword.createKeyword("자바", "자바에 대한 설명", 1, 1, sessionId, null)); + final Keyword child1 = createKeywordChildren( + Keyword.createKeyword("List", "List에 대한 설명", 1, 1, sessionId, parent)); + createKeywordChildren( + Keyword.createKeyword("Set", "Set에 대한 설명", 2, 1, sessionId, parent)); - final Keyword keyword1 = keywordRepository.save( - Keyword.createKeyword("자바1", "자바 설명1", 1, 5, session1.getId(), null)); - final Keyword keyword2 = keywordRepository.save( - Keyword.createKeyword("자바2", "자바 설명2", 2, 5, session1.getId(), keyword1)); - final Keyword keyword3 = keywordRepository.save( - Keyword.createKeyword("자바3", "자바 설명3", 3, 5, session1.getId(), null)); - final Keyword keyword4 = keywordRepository.save( - Keyword.createKeyword("자바4", "자바 설명4", 4, 5, session1.getId(), keyword3)); - final Keyword keyword5 = keywordRepository.save( - Keyword.createKeyword("자바5", "자바 설명5", 5, 5, session2.getId(), null)); - final Keyword keyword6 = keywordRepository.save( - Keyword.createKeyword("자바6", "자바 설명6", 6, 5, session2.getId(), keyword1)); - final Keyword keyword7 = keywordRepository.save( - Keyword.createKeyword("자바7", "자바 설명7", 7, 5, session2.getId(), null)); - final Keyword keyword8 = keywordRepository.save( - Keyword.createKeyword("자바8", "자바 설명8", 8, 5, session3.getId(), keyword2)); - final Keyword keyword9 = keywordRepository.save( - Keyword.createKeyword("자바9", "자바 설명9", 9, 5, session4.getId(), keyword2)); - final Keyword keyword10 = keywordRepository.save( - Keyword.createKeyword("자바10", "자바 설명10", 10, 5, session5.getId(), null)); + quizRepository.save(new Quiz(parent, "자바란 무엇일까요?")); + quizRepository.save(new Quiz(parent, "자바를 왜 쓰죠?")); + quizRepository.save(new Quiz(child1, "리스트보단 배열이 낫지 않나요?")); //when - final List keywords = keywordRepository.newFindByParentIsNull(); + final List totalQuizCounts = keywordRepository.findTotalQuizCount(); //then - assertThat(keywords) - .usingRecursiveComparison() - .ignoringFields("id", "parent.id") - .isEqualTo(Arrays.asList(keyword1, keyword3, keyword5, keyword7, keyword10)); + assertSoftly(softAssertions -> { + softAssertions.assertThat(totalQuizCounts) + .map(KeywordIdAndTotalQuizCount::getKeywordId) + .containsExactlyInAnyOrder(parent.getId(), child1.getId()); + + softAssertions.assertThat(totalQuizCounts) + .map(KeywordIdAndTotalQuizCount::getTotalQuizCount) + .containsExactlyInAnyOrder(1, 2); + }); + } + + @Test + @DisplayName("회원의 id로 각 키워드별 완료한 답변 개수를 조회할 수 있다") + void findDoneQuizCountByMemberId() { + //given + final Long sessionId = createSession(); + + final Keyword parent = createKeywordParent( + Keyword.createKeyword("자바", "자바에 대한 설명", 1, 1, sessionId, null)); + final Keyword child1 = createKeywordChildren( + Keyword.createKeyword("List", "List에 대한 설명", 1, 1, sessionId, parent)); + + final Quiz quiz1 = quizRepository.save(new Quiz(parent, "자바란 무엇일까요?")); + final Quiz quiz2 = quizRepository.save(new Quiz(parent, "코틀린은 뭘까요?")); + quizRepository.save(new Quiz(child1, "리스트보단 배열이 낫지 않나요?")); + + final Member member = memberRepository.save(new Member("username1", "nickname1", Role.CREW, 1L, "imageUrl1")); + essayAnswerRepository.save(new EssayAnswer(quiz1, "쓰라고 해서요ㅠ", member)); + essayAnswerRepository.save(new EssayAnswer(quiz2, "쓰라고 해서요ㅠ", member)); + + //when + final List doneQuizCounts = keywordRepository.findAnsweredQuizCountByMemberId(member.getId()); + + //then + assertSoftly(softAssertions -> { + softAssertions.assertThat(doneQuizCounts) + .map(KeywordIdAndAnsweredQuizCount::getKeywordId) + .containsExactly(parent.getId()); + softAssertions.assertThat(doneQuizCounts) + .map(KeywordIdAndAnsweredQuizCount::getAnsweredQuizCount) + .containsExactly(2); + }); + } + + @Test + @DisplayName("커리큘럼 ID에 해당하는 모든 키워드들을 조회할 수 있다") + void findAllByCurriculumId() { + //given + final Long session1 = createSessionWithCurriculumId(1L); + final Long session2 = createSessionWithCurriculumId(1L); + + final Keyword parent1 = createKeywordParent( + Keyword.createKeyword("자바", "자바에 대한 설명", 1, 1, session1, null)); + createKeywordChildren( + Keyword.createKeyword("List", "List에 대한 설명", 1, 1, session1, parent1)); + createKeywordChildren( + Keyword.createKeyword("List", "List에 대한 설명", 1, 1, session1, parent1)); + + createKeywordParent( + Keyword.createKeyword("자바", "자바에 대한 설명", 1, 1, session2, null)); + + //when + final List keywords = keywordRepository.findAllByCurriculumId(1L); + + //then + assertThat(keywords).hasSize(4); + } + + private Long createSession() { + Session session = new Session("테스트 세션"); + sessionRepository.save(session); + return session.getId(); + } + + private Long createSessionWithCurriculumId(final Long curriculumId) { + Session session = new Session(curriculumId, "테스트 세션"); + sessionRepository.save(session); + return session.getId(); } private Keyword createKeywordParent(final Keyword keyword) {