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

전체 키워드 조회 시 학습 진도를 포함한다 #1510

Merged
merged 6 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ public class KeywordDocumentation extends NewDocumentation {
"자바에 대한 설명을 작성했습니다.",
1,
1,
0,
0,
null,
null,
null
Expand All @@ -134,6 +136,8 @@ public class KeywordDocumentation extends NewDocumentation {
"자바에 대한 설명을 작성했습니다.",
1,
1,
0,
0,
null,
null,
new HashSet<>(
Expand All @@ -145,7 +149,6 @@ public class KeywordDocumentation extends NewDocumentation {
1,
1,
1L,
null,
null
),
new KeywordResponse(
Expand All @@ -155,7 +158,6 @@ public class KeywordDocumentation extends NewDocumentation {
2,
1,
1L,
null,
null
))
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
package wooteco.prolog.roadmap.application;

import static wooteco.prolog.common.exception.BadRequestCode.CURRICULUM_NOT_FOUND_EXCEPTION;

import java.util.List;
import java.util.Set;
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 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;

@RequiredArgsConstructor
@Transactional
@Service
Expand All @@ -25,19 +35,82 @@ 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 findAllKeywords(final Long curriculumId) {
public KeywordsResponse findAllKeywordsWithProgress(final Long curriculumId, final Long memberId) {
final Curriculum curriculum = curriculumRepository.findById(curriculumId)
.orElseThrow(() -> new BadRequestException(CURRICULUM_NOT_FOUND_EXCEPTION));

final List<Keyword> keywordsInCurriculum = getKeywordsInCurriculum(curriculum);

final Map<Keyword, Set<Quiz>> quizzesInKeywords = quizRepository.findAll().stream()
.collect(groupingBy(Quiz::getKeyword, toSet()));

return createResponsesWithProgress(keywordsInCurriculum, quizzesInKeywords, getDoneQuizzes(memberId));
}

private Set<Quiz> getDoneQuizzes(final Long memberId) {
return essayAnswerRepository.findAllByMemberId(memberId).stream()
.map(EssayAnswer::getQuiz)
.collect(toSet());
}

private List<Keyword> getKeywordsInCurriculum(final Curriculum curriculum) {
final Set<Long> sessionIds = sessionRepository.findAllByCurriculumId(curriculum.getId())
.stream()
.map(Session::getId)
.collect(Collectors.toSet());
.collect(toSet());

return keywordRepository.findBySessionIdIn(sessionIds);
}

private KeywordsResponse createResponsesWithProgress(final List<Keyword> keywords,
final Map<Keyword, Set<Quiz>> quizzesPerKeyword,
final Set<Quiz> doneQuizzes) {
final List<KeywordResponse> keywordResponses = keywords.stream()
.filter(Keyword::isRoot)
.map(keyword -> createResponseWithProgress(keyword, quizzesPerKeyword, doneQuizzes))
.collect(toList());

return new KeywordsResponse(keywordResponses);
}

private KeywordResponse createResponseWithProgress(final Keyword keyword,
final Map<Keyword, Set<Quiz>> quizzesPerKeyword,
final Set<Quiz> doneQuizzes) {
final int totalQuizCount = quizzesPerKeyword.get(keyword).size();
final int doneQuizCount = getDoneQuizCount(quizzesPerKeyword.get(keyword), doneQuizzes);

final List<RecommendedPostResponse> recommendedPostResponses = keyword.getRecommendedPosts().stream()
.map(RecommendedPostResponse::from)
.collect(toList());

final List<Keyword> keywords = keywordRepository.findBySessionIdIn(sessionIds);
return new KeywordResponse(
keyword.getId(),
keyword.getName(),
keyword.getDescription(),
keyword.getSeq(),
keyword.getImportance(),
totalQuizCount,
doneQuizCount,
keyword.getParentIdOrNull(),
recommendedPostResponses,
createChildrenWithProgress(keyword.getChildren(), quizzesPerKeyword, doneQuizzes)
);
}

private int getDoneQuizCount(final Set<Quiz> quizzes, final Set<Quiz> doneQuizzes) {
quizzes.retainAll(doneQuizzes);
return quizzes.size();
}

return KeywordsResponse.createResponseWithChildren(keywords);
private Set<KeywordResponse> createChildrenWithProgress(final Set<Keyword> children,
final Map<Keyword, Set<Quiz>> quizzesPerKeyword,
final Set<Quiz> userAnswers) {
return children.stream()
.map(child -> createResponseWithProgress(child, quizzesPerKeyword, userAnswers))
.collect(toSet());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@
import lombok.NoArgsConstructor;
import wooteco.prolog.roadmap.domain.Keyword;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import static java.util.Collections.*;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class KeywordResponse {
Expand All @@ -19,32 +25,44 @@ public class KeywordResponse {
private String description;
private int order;
private int importance;
private int totalQuizCount;
private int doneQuizCount;
private Long parentKeywordId;
private List<RecommendedPostResponse> recommendedPosts;
private Set<KeywordResponse> childrenKeywords;

public KeywordResponse(final Long keywordId, final String name, final String description,
final int order,
final int importance, final Long parentKeywordId,
final int order, final int importance, final int totalQuizCount,
final int doneQuizCount, final Long parentKeywordId,
final List<RecommendedPostResponse> recommendedPosts,
final Set<KeywordResponse> childrenKeywords) {
this.keywordId = keywordId;
this.name = name;
this.description = description;
this.order = order;
this.importance = importance;
this.totalQuizCount = totalQuizCount;
this.doneQuizCount = doneQuizCount;
this.parentKeywordId = parentKeywordId;
this.recommendedPosts = recommendedPosts;
this.childrenKeywords = childrenKeywords;
}

public KeywordResponse(final Long keywordId, final String name, final String description,
final int order,
final int importance, final Long parentKeywordId,
final Set<KeywordResponse> childrenKeywords) {
this(keywordId, name, description, order, importance, 0, 0, parentKeywordId, emptyList(), childrenKeywords);
}

public static KeywordResponse createResponse(final Keyword keyword) {
return new KeywordResponse(
keyword.getId(),
keyword.getName(),
keyword.getDescription(),
keyword.getSeq(),
keyword.getImportance(),
0, 0,
keyword.getParentIdOrNull(),
createRecommendedPostResponses(keyword),
null);
Expand All @@ -64,11 +82,10 @@ public static KeywordResponse createWithAllChildResponse(final Keyword keyword)
keyword.getSeq(),
keyword.getImportance(),
keyword.getParentIdOrNull(),
createRecommendedPostResponses(keyword),
createKeywordChild(keyword.getChildren()));
createChildren(keyword.getChildren()));
}

private static Set<KeywordResponse> createKeywordChild(final Set<Keyword> children) {
private static Set<KeywordResponse> createChildren(final Set<Keyword> children) {
Set<KeywordResponse> keywords = new HashSet<>();
for (Keyword keyword : children) {
keywords.add(createWithAllChildResponse(keyword));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package wooteco.prolog.roadmap.domain.repository;

import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.query.Param;
import wooteco.prolog.roadmap.domain.EssayAnswer;

import java.util.List;
import java.util.Optional;
import java.util.Set;

public interface EssayAnswerRepository extends JpaRepository<EssayAnswer, Long>,
JpaSpecificationExecutor<EssayAnswer> {

Expand All @@ -16,4 +19,9 @@ public interface EssayAnswerRepository extends JpaRepository<EssayAnswer, Long>,
Optional<EssayAnswer> findByIdAndMemberId(Long id, Long memberId);

List<EssayAnswer> findByQuizIdOrderByIdDesc(Long quizId);

@Query("SELECT e FROM EssayAnswer e " +
"LEFT JOIN FETCH e.quiz q " +
"WHERE e.member.id = :memberId")
Set<EssayAnswer> findAllByMemberId(@Param("memberId") Long memberId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@param 을 쓰지 않아도 변수명을 기반으로 :memberId 를 추론해주는 것으로 아닌데 명시적으로 표기하는 것을 선호하시나요??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컴파일러에게 -parameters 옵션을 전달해주어야 내부적으로 바인딩을 합니다.
그래서 저는 컴파일 옵션의 컨디션과 상관 없이 동작하는 것을 선호합니다!

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import wooteco.prolog.login.domain.AuthMemberPrincipal;
import wooteco.prolog.login.ui.LoginMember;
import wooteco.prolog.roadmap.application.RoadMapService;
import wooteco.prolog.roadmap.application.dto.KeywordsResponse;

Expand All @@ -14,7 +16,8 @@ public class RoadmapController {
private final RoadMapService roadMapService;

@GetMapping("/roadmaps")
public KeywordsResponse findRoadMapKeyword(@RequestParam final Long curriculumId) {
return roadMapService.findAllKeywords(curriculumId);
public KeywordsResponse findRoadMapKeyword(@AuthMemberPrincipal final LoginMember member,
@RequestParam final Long curriculumId) {
return roadMapService.findAllKeywordsWithProgress(curriculumId, member.getId());
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,38 @@
package wooteco.prolog.roadmap.application;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.when;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
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 wooteco.prolog.member.domain.Member;
import wooteco.prolog.member.domain.Role;
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 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)
class RoadMapServiceTest {

Expand All @@ -32,18 +42,25 @@ class RoadMapServiceTest {
private SessionRepository sessionRepository;
@Mock
private KeywordRepository keywordRepository;
@Mock
private QuizRepository quizRepository;
@Mock
private EssayAnswerRepository essayAnswerRepository;
@InjectMocks
private RoadMapService roadMapService;

@Test
@DisplayName("curriculumId가 주어지면 해당 커리큘럼의 키워드들을 전부 조회할 수 있다.")
void findAllKeywords() {
@DisplayName("curriculumId가 주어지면 해당 커리큘럼의 키워드들을 학습 진도와 함께 전부 조회할 수 있다.")
void findAllKeywordsWithProgress() {
//given
final Curriculum curriculum = new Curriculum(1L, "커리큘럼1");
final Session session = new Session(1L, curriculum.getId(), "세션1");
final List<Session> 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);

when(curriculumRepository.findById(anyLong()))
.thenReturn(Optional.of(curriculum));
Expand All @@ -54,15 +71,22 @@ void findAllKeywords() {
when(keywordRepository.findBySessionIdIn(any()))
.thenReturn(Arrays.asList(keyword));

final KeywordsResponse expected = KeywordsResponse.createResponseWithChildren(Arrays.asList(keyword));
when(essayAnswerRepository.findAllByMemberId(1L))
.thenReturn(new HashSet<>(Arrays.asList(essayAnswer)));

when(quizRepository.findAll())
.thenReturn(Arrays.asList(quiz));

//when
final KeywordsResponse actual =
roadMapService.findAllKeywords(curriculum.getId());
roadMapService.findAllKeywordsWithProgress(curriculum.getId(), 1L);

//then
assertThat(actual)
.usingRecursiveComparison()
.isEqualTo(expected);
final List<KeywordResponse> responses = actual.getData();
assertSoftly(soft -> {
assertThat(responses).hasSize(1);
assertThat(responses.get(0).getDoneQuizCount()).isOne();
assertThat(responses.get(0).getTotalQuizCount()).isOne();
});
}
}
Loading