diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/steps/KeywordRecommendedPostStepDefinitions.java b/backend/src/acceptanceTest/java/wooteco/prolog/steps/KeywordRecommendedPostStepDefinitions.java new file mode 100644 index 000000000..2dfafcb0f --- /dev/null +++ b/backend/src/acceptanceTest/java/wooteco/prolog/steps/KeywordRecommendedPostStepDefinitions.java @@ -0,0 +1,58 @@ +package wooteco.prolog.steps; + +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import org.springframework.http.HttpStatus; +import wooteco.prolog.AcceptanceSteps; +import wooteco.prolog.roadmap.application.dto.RecommendedRequest; + +import static org.assertj.core.api.Assertions.assertThat; +import static wooteco.prolog.fixtures.KeywordAcceptanceFixture.KEYWORD_REQUEST; + +public class KeywordRecommendedPostStepDefinitions extends AcceptanceSteps { + + @Given("{int}번 키워드에 대해 추천 포스트 {string}를 작성하고") + @When("{int}번 키워드에 대해 추천 포스트 {string}를 작성하면") + public void 추천_포스트를_추가하면(int keywordId, String url) { + context.invokeHttpPost( + "/keywords/"+keywordId+"/recommended-posts", + new RecommendedRequest(url) + ); + } + + @When("{int}번 키워드에 대한 {int}번 추천 포스트를 {string}로 수정하면") + public void 추천_포스트를_수정하면(int keywordId, int recommendedId, String url) { + context.invokeHttpPut( + "/keywords/"+keywordId+"/recommended-posts/"+recommendedId, + new RecommendedRequest(url)); + } + + @When("{int}번 키워드에 대한 {int}번 추천 포스트를 삭제하면") + public void 추천_포스트를_삭제하면(int keywordId, int recommendedId) { + context.invokeHttpDelete( + "/keywords/" + keywordId + "/recommended-posts/" + recommendedId + ); + } + + @Then("추천 포스트가 생성된다") + public void 추천_포스트가_생성된다() { + int statusCode = context.response.statusCode(); + + assertThat(statusCode).isEqualTo(HttpStatus.CREATED.value()); + } + + @Then("추천 포스트가 수정된다") + public void 추천_포스트가_수정된다() { + int statusCode = context.response.statusCode(); + + assertThat(statusCode).isEqualTo(HttpStatus.OK.value()); + } + + @Then("추천 포스트가 삭제된다") + public void 추천_포스트가_삭제된다() { + int statusCode = context.response.statusCode(); + + assertThat(statusCode).isEqualTo(HttpStatus.NO_CONTENT.value()); + } +} diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/steps/KeywordStepDefinitions.java b/backend/src/acceptanceTest/java/wooteco/prolog/steps/KeywordStepDefinitions.java index 4aff5b7f7..d6d5ef276 100644 --- a/backend/src/acceptanceTest/java/wooteco/prolog/steps/KeywordStepDefinitions.java +++ b/backend/src/acceptanceTest/java/wooteco/prolog/steps/KeywordStepDefinitions.java @@ -1,8 +1,5 @@ package wooteco.prolog.steps; -import static org.assertj.core.api.Assertions.assertThat; -import static wooteco.prolog.fixtures.KeywordAcceptanceFixture.KEYWORD_REQUEST; - import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; import io.cucumber.java.en.When; @@ -10,6 +7,9 @@ import wooteco.prolog.AcceptanceSteps; import wooteco.prolog.session.application.dto.SessionRequest; +import static org.assertj.core.api.Assertions.assertThat; +import static wooteco.prolog.fixtures.KeywordAcceptanceFixture.KEYWORD_REQUEST; + public class KeywordStepDefinitions extends AcceptanceSteps { /** diff --git a/backend/src/acceptanceTest/resources/wooteco/prolog/keyword-recommended-post.feature b/backend/src/acceptanceTest/resources/wooteco/prolog/keyword-recommended-post.feature new file mode 100644 index 000000000..9be2ee3e6 --- /dev/null +++ b/backend/src/acceptanceTest/resources/wooteco/prolog/keyword-recommended-post.feature @@ -0,0 +1,20 @@ +@api +Feature: 로드맵 키워드 추천 포스트 관련 기능 + + Background: 사전 작업 + Given "2022 백엔드 레벨1" 세션을 생성하고 - 1번 세션 + And 1번 세션에 "자바"라는 키워드를 순서 1, 중요도 2로 작성하고 + + Scenario: 키워드 추천 포스트 생성하기 + When 1번 키워드에 대해 추천 포스트 "https://javajavajava"를 작성하면 + Then 추천 포스트가 생성된다 + + Scenario: 키워드 추천 포스트 수정하기 + Given 1번 키워드에 대해 추천 포스트 "https://javajavajava"를 작성하고 + When 1번 키워드에 대한 1번 추천 포스트를 "https://java2java2"로 수정하면 + Then 추천 포스트가 수정된다 + + Scenario: 키워드 추천 포스트 삭제하기 + Given 1번 키워드에 대해 추천 포스트 "https://javajavajava"를 작성하고 + When 1번 키워드에 대한 1번 추천 포스트를 삭제하면 + Then 추천 포스트가 삭제된다 diff --git a/backend/src/documentation/java/wooteco/prolog/docu/KeywordDocumentation.java b/backend/src/documentation/java/wooteco/prolog/docu/KeywordDocumentation.java index dd51f1199..f98318924 100644 --- a/backend/src/documentation/java/wooteco/prolog/docu/KeywordDocumentation.java +++ b/backend/src/documentation/java/wooteco/prolog/docu/KeywordDocumentation.java @@ -1,12 +1,5 @@ package wooteco.prolog.docu; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.doNothing; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; - -import java.util.Arrays; -import java.util.HashSet; import org.elasticsearch.common.collect.List; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -21,6 +14,14 @@ import wooteco.prolog.roadmap.application.dto.KeywordsResponse; import wooteco.prolog.roadmap.ui.KeywordController; +import java.util.Arrays; +import java.util.HashSet; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; + @WebMvcTest(controllers = KeywordController.class) public class KeywordDocumentation extends NewDocumentation { @@ -108,6 +109,7 @@ public class KeywordDocumentation extends NewDocumentation { 1, 1, null, + null, null ); @@ -133,6 +135,7 @@ public class KeywordDocumentation extends NewDocumentation { 1, 1, null, + null, new HashSet<>( Arrays.asList( new KeywordResponse( @@ -142,6 +145,7 @@ public class KeywordDocumentation extends NewDocumentation { 1, 1, 1L, + null, null ), new KeywordResponse( @@ -151,6 +155,7 @@ public class KeywordDocumentation extends NewDocumentation { 2, 1, 1L, + null, null )) ) diff --git a/backend/src/main/java/wooteco/prolog/common/exception/BadRequestCode.java b/backend/src/main/java/wooteco/prolog/common/exception/BadRequestCode.java index 63d12ffda..430ab9861 100644 --- a/backend/src/main/java/wooteco/prolog/common/exception/BadRequestCode.java +++ b/backend/src/main/java/wooteco/prolog/common/exception/BadRequestCode.java @@ -2,6 +2,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; +import wooteco.prolog.roadmap.domain.RecommendedPost; import wooteco.prolog.session.domain.Mission; import wooteco.prolog.session.domain.Session; import wooteco.prolog.studylog.domain.TagName; @@ -70,12 +71,15 @@ public enum BadRequestCode { NOT_EMPTY_ESSAY_ANSWER_EXCEPTION(8013, "답변은 공백일 수 없습니다."), ESSAY_ANSWER_NOT_VALID_USER(8014, "본인이 작성한 답변만 수정할 수 있습니다."), + ROADMAP_RECOMMENDED_POST_NOT_FOUND(8101, "해당 추천 포스트가 존재하지 않습니다."), + ROADMAP_RECOMMENDED_POST_INVALID_URL_LENGTH(8102, String.format( + "해당 추천 포스트의 URL 길이는 1 ~ %d여야 합니다.", RecommendedPost.URL_LENGTH_UPPER_BOUND)), + FILE_NAME_EMPTY_EXCEPTION(9001, "파일 이름이 존재하지 않습니다."), UNSUPPORTED_FILE_EXTENSION_EXCEPTION(9002, "지원하지 않는 파일 확장자입니다."), FILE_UPLOAD_FAIL_EXCEPTION(9003, "파일 업로드에 실패했습니다."), DUPLICATE_SESSION_EXCEPTION(10001, "중복되는 세션입니다."), - SESSION_NOT_FOUND_EXCEPTION(10002, "세션을 찾을 수 없습니다."), TOO_LONG_LEVEL_NAME_EXCEPTION(10003, String.format("세션 이름이 %d자 초과입니다.", Session.MAX_LENGTH)), SEARCH_ARGUMENT_PARSE_EXCEPTION(11001, "parsing 할 수 없는 argument입니다."), diff --git a/backend/src/main/java/wooteco/prolog/roadmap/application/EssayAnswerService.java b/backend/src/main/java/wooteco/prolog/roadmap/application/EssayAnswerService.java index 5102e0629..3042eab57 100644 --- a/backend/src/main/java/wooteco/prolog/roadmap/application/EssayAnswerService.java +++ b/backend/src/main/java/wooteco/prolog/roadmap/application/EssayAnswerService.java @@ -1,23 +1,34 @@ package wooteco.prolog.roadmap.application; +import static wooteco.prolog.common.exception.BadRequestCode.CURRICULUM_NOT_FOUND_EXCEPTION; import static wooteco.prolog.common.exception.BadRequestCode.ESSAY_ANSWER_NOT_FOUND_EXCEPTION; import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_QUIZ_NOT_FOUND_EXCEPTION; +import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_SESSION_NOT_FOUND_EXCEPTION; +import java.util.List; +import java.util.stream.Collectors; import org.hibernate.Hibernate; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import wooteco.prolog.common.exception.BadRequestException; import wooteco.prolog.member.application.MemberService; import wooteco.prolog.member.domain.Member; import wooteco.prolog.roadmap.application.dto.EssayAnswerRequest; +import wooteco.prolog.roadmap.application.dto.EssayAnswerSearchRequest; import wooteco.prolog.roadmap.application.dto.EssayAnswerUpdateRequest; +import wooteco.prolog.roadmap.domain.Curriculum; import wooteco.prolog.roadmap.domain.EssayAnswer; 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.EssayAnswerSpecification; import wooteco.prolog.roadmap.domain.repository.QuizRepository; - -import java.util.List; +import wooteco.prolog.session.domain.Session; +import wooteco.prolog.session.domain.repository.SessionRepository; +import wooteco.prolog.studylog.application.dto.EssayAnswersResponse; @Transactional @Service @@ -26,14 +37,20 @@ public class EssayAnswerService { private final EssayAnswerRepository essayAnswerRepository; private final QuizRepository quizRepository; private final MemberService memberService; - - @Autowired - public EssayAnswerService(EssayAnswerRepository essayAnswerRepository, - QuizRepository quizRepository, - MemberService memberService) { + private final CurriculumRepository curriculumRepository; + private final SessionRepository sessionRepository; + + public EssayAnswerService( + EssayAnswerRepository essayAnswerRepository, + QuizRepository quizRepository, + MemberService memberService, + CurriculumRepository curriculumRepository, + SessionRepository sessionRepository) { this.essayAnswerRepository = essayAnswerRepository; this.quizRepository = quizRepository; this.memberService = memberService; + this.curriculumRepository = curriculumRepository; + this.sessionRepository = sessionRepository; } @Transactional @@ -85,4 +102,35 @@ public List findByQuizId(Long quizId) { return essayAnswers; } + + public EssayAnswersResponse searchEssayAnswers( + final EssayAnswerSearchRequest request, + final Pageable pageable + ) { + + final Long curriculumId = request.getCurriculumId(); + + final Curriculum curriculum = curriculumRepository.findById(curriculumId) + .orElseThrow(() -> new BadRequestException(CURRICULUM_NOT_FOUND_EXCEPTION)); + + final List sessionIds = sessionRepository.findAllByCurriculumId(curriculum.getId()) + .stream() + .map(Session::getId) + .collect(Collectors.toList()); + + if (sessionIds.isEmpty()) { + throw new BadRequestException(ROADMAP_SESSION_NOT_FOUND_EXCEPTION); + } + + final Specification essayAnswerSpecification = EssayAnswerSpecification.equalsSessionIdsIn( + sessionIds) + .and(EssayAnswerSpecification.equalsKeywordId(request.getKeywordId())) + .and(EssayAnswerSpecification.inQuizIds(request.getQuizIds())) + .and(EssayAnswerSpecification.inMemberIds(request.getMemberIds())) + .and(EssayAnswerSpecification.orderByIdDesc()); + + final Page essayAnswers = essayAnswerRepository.findAll(essayAnswerSpecification, + pageable); + return EssayAnswersResponse.of(essayAnswers); + } } 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 5d0578e48..87ea445b4 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,11 @@ import wooteco.prolog.roadmap.domain.repository.KeywordRepository; import wooteco.prolog.session.domain.repository.SessionRepository; +import java.util.List; + +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 { @@ -55,7 +56,7 @@ public KeywordResponse findKeywordWithAllChild(final Long sessionId, final Long existSession(sessionId); existKeyword(keywordId); - Keyword keyword = keywordRepository.findFetchById(keywordId); + Keyword keyword = keywordRepository.findFetchByIdOrderBySeq(keywordId); return KeywordResponse.createWithAllChildResponse(keyword); } @@ -82,7 +83,7 @@ public void updateKeyword(final Long sessionId, final Long keywordId, public void deleteKeyword(final Long sessionId, final Long keywordId) { existSession(sessionId); - Keyword keyword = keywordRepository.findFetchById(keywordId); + Keyword keyword = keywordRepository.findFetchByIdOrderBySeq(keywordId); keywordRepository.delete(keyword); } diff --git a/backend/src/main/java/wooteco/prolog/roadmap/application/RecommendedPostService.java b/backend/src/main/java/wooteco/prolog/roadmap/application/RecommendedPostService.java new file mode 100644 index 000000000..6d3c00cc1 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/roadmap/application/RecommendedPostService.java @@ -0,0 +1,59 @@ +package wooteco.prolog.roadmap.application; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import wooteco.prolog.common.exception.BadRequestException; +import wooteco.prolog.roadmap.application.dto.RecommendedRequest; +import wooteco.prolog.roadmap.application.dto.RecommendedUpdateRequest; +import wooteco.prolog.roadmap.domain.Keyword; +import wooteco.prolog.roadmap.domain.RecommendedPost; +import wooteco.prolog.roadmap.domain.repository.KeywordRepository; +import wooteco.prolog.roadmap.domain.repository.RecommendedPostRepository; + +import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_KEYWORD_NOT_FOUND_EXCEPTION; +import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_RECOMMENDED_POST_NOT_FOUND; + +@Transactional(readOnly = true) +@Service +public class RecommendedPostService { + + private final RecommendedPostRepository recommendedPostRepository; + private final KeywordRepository keywordRepository; + + public RecommendedPostService(final RecommendedPostRepository recommendedPostRepository, + final KeywordRepository keywordRepository) { + this.recommendedPostRepository = recommendedPostRepository; + this.keywordRepository = keywordRepository; + } + + @Transactional + public Long create(final Long keywordId, final RecommendedRequest request) { + final Keyword keyword = findKeywordOrThrow(keywordId); + final RecommendedPost post = new RecommendedPost(request.getUrl(), keyword); + + return recommendedPostRepository.save(post).getId(); + } + + private Keyword findKeywordOrThrow(final Long keywordId) { + return keywordRepository.findById(keywordId) + .orElseThrow(() -> new BadRequestException(ROADMAP_KEYWORD_NOT_FOUND_EXCEPTION)); + } + + @Transactional + public void update(final Long recommendedId, final RecommendedUpdateRequest request) { + final RecommendedPost post = findPostOrThrow(recommendedId); + + post.updateUrl(request.getUrl()); + } + + private RecommendedPost findPostOrThrow(final Long recommendedId) { + return recommendedPostRepository.findById(recommendedId) + .orElseThrow(() -> new BadRequestException(ROADMAP_RECOMMENDED_POST_NOT_FOUND)); + } + + @Transactional + public void delete(final Long recommendedId) { + final RecommendedPost recommendedPost = findPostOrThrow(recommendedId); + recommendedPost.remove(); + } +} 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 a8f1b30c1..d352c1dc2 100644 --- a/backend/src/main/java/wooteco/prolog/roadmap/application/RoadMapService.java +++ b/backend/src/main/java/wooteco/prolog/roadmap/application/RoadMapService.java @@ -1,11 +1,14 @@ 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.KeywordsResponse; import wooteco.prolog.roadmap.domain.Curriculum; import wooteco.prolog.roadmap.domain.Keyword; @@ -26,8 +29,7 @@ public class RoadMapService { @Transactional(readOnly = true) public KeywordsResponse findAllKeywords(final Long curriculumId) { final Curriculum curriculum = curriculumRepository.findById(curriculumId) - .orElseThrow(() -> new IllegalArgumentException( - "해당 커리큘럼이 존재하지 않습니다. curriculumId = " + curriculumId)); + .orElseThrow(() -> new BadRequestException(CURRICULUM_NOT_FOUND_EXCEPTION)); final Set sessionIds = sessionRepository.findAllByCurriculumId(curriculum.getId()) .stream() diff --git a/backend/src/main/java/wooteco/prolog/roadmap/application/dto/EssayAnswerResponse.java b/backend/src/main/java/wooteco/prolog/roadmap/application/dto/EssayAnswerResponse.java index ecf280e12..360bbb252 100644 --- a/backend/src/main/java/wooteco/prolog/roadmap/application/dto/EssayAnswerResponse.java +++ b/backend/src/main/java/wooteco/prolog/roadmap/application/dto/EssayAnswerResponse.java @@ -2,11 +2,13 @@ import java.time.LocalDateTime; import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import wooteco.prolog.member.application.dto.MemberResponse; import wooteco.prolog.roadmap.domain.EssayAnswer; +@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PRIVATE) @Getter public class EssayAnswerResponse { diff --git a/backend/src/main/java/wooteco/prolog/roadmap/application/dto/EssayAnswerSearchRequest.java b/backend/src/main/java/wooteco/prolog/roadmap/application/dto/EssayAnswerSearchRequest.java new file mode 100644 index 000000000..289dd382f --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/roadmap/application/dto/EssayAnswerSearchRequest.java @@ -0,0 +1,20 @@ +package wooteco.prolog.roadmap.application.dto; + +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Setter +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class EssayAnswerSearchRequest { + + private Long curriculumId; + private Long keywordId; + private List quizIds; + private List memberIds; +} 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 68069a1d9..0e8d105fc 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,12 +1,15 @@ package wooteco.prolog.roadmap.application.dto; -import java.util.HashSet; -import java.util.Set; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import wooteco.prolog.roadmap.domain.Keyword; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter public class KeywordResponse { @@ -17,11 +20,13 @@ public class KeywordResponse { private int order; private int importance; private Long parentKeywordId; + private List recommendedPosts; private Set childrenKeywords; public KeywordResponse(final Long keywordId, final String name, final String description, final int order, final int importance, final Long parentKeywordId, + final List recommendedPosts, final Set childrenKeywords) { this.keywordId = keywordId; this.name = name; @@ -29,6 +34,7 @@ public KeywordResponse(final Long keywordId, final String name, final String des this.order = order; this.importance = importance; this.parentKeywordId = parentKeywordId; + this.recommendedPosts = recommendedPosts; this.childrenKeywords = childrenKeywords; } @@ -40,9 +46,16 @@ public static KeywordResponse createResponse(final Keyword keyword) { keyword.getSeq(), keyword.getImportance(), keyword.getParentIdOrNull(), + createRecommendedPostResponses(keyword), null); } + private static List createRecommendedPostResponses(final Keyword keyword) { + return keyword.getRecommendedPosts().stream() + .map(RecommendedPostResponse::from) + .collect(Collectors.toList()); + } + public static KeywordResponse createWithAllChildResponse(final Keyword keyword) { return new KeywordResponse( keyword.getId(), @@ -51,6 +64,7 @@ public static KeywordResponse createWithAllChildResponse(final Keyword keyword) keyword.getSeq(), keyword.getImportance(), keyword.getParentIdOrNull(), + createRecommendedPostResponses(keyword), createKeywordChild(keyword.getChildren())); } 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 bbd9c3018..d904b67a3 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 @@ -26,7 +26,7 @@ public static KeywordsResponse createResponse(final List keywords) { public static KeywordsResponse createResponseWithChildren(final List keywords) { List keywordsResponse = keywords.stream() - .filter(it -> it.getParent() == null) + .filter(Keyword::isRoot) .map(KeywordResponse::createWithAllChildResponse) .collect(Collectors.toList()); return new KeywordsResponse(keywordsResponse); diff --git a/backend/src/main/java/wooteco/prolog/roadmap/application/dto/RecommendedPostResponse.java b/backend/src/main/java/wooteco/prolog/roadmap/application/dto/RecommendedPostResponse.java new file mode 100644 index 000000000..75816135e --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/roadmap/application/dto/RecommendedPostResponse.java @@ -0,0 +1,17 @@ +package wooteco.prolog.roadmap.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import wooteco.prolog.roadmap.domain.RecommendedPost; + +@AllArgsConstructor +@Getter +public class RecommendedPostResponse { + + private final Long id; + private final String url; + + public static RecommendedPostResponse from(final RecommendedPost recommendedPost) { + return new RecommendedPostResponse(recommendedPost.getId(), recommendedPost.getUrl()); + } +} diff --git a/backend/src/main/java/wooteco/prolog/roadmap/application/dto/RecommendedRequest.java b/backend/src/main/java/wooteco/prolog/roadmap/application/dto/RecommendedRequest.java new file mode 100644 index 000000000..a515dfb65 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/roadmap/application/dto/RecommendedRequest.java @@ -0,0 +1,14 @@ +package wooteco.prolog.roadmap.application.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class RecommendedRequest { + + private String url; +} diff --git a/backend/src/main/java/wooteco/prolog/roadmap/application/dto/RecommendedUpdateRequest.java b/backend/src/main/java/wooteco/prolog/roadmap/application/dto/RecommendedUpdateRequest.java new file mode 100644 index 000000000..44b2db78f --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/roadmap/application/dto/RecommendedUpdateRequest.java @@ -0,0 +1,14 @@ +package wooteco.prolog.roadmap.application.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class RecommendedUpdateRequest { + + private String url; +} 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 8518c806c..9fc136b14 100644 --- a/backend/src/main/java/wooteco/prolog/roadmap/domain/Keyword.java +++ b/backend/src/main/java/wooteco/prolog/roadmap/domain/Keyword.java @@ -1,23 +1,6 @@ package wooteco.prolog.roadmap.domain; -import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_KEYWORD_AND_KEYWORD_PARENT_SAME_EXCEPTION; -import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_KEYWORD_SEQUENCE_EXCEPTION; - -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; -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 lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -26,21 +9,11 @@ import javax.persistence.*; import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; - -import javax.persistence.*; -import java.util.HashSet; -import java.util.List; import java.util.Objects; import java.util.Set; -import javax.persistence.*; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; +import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_KEYWORD_AND_KEYWORD_PARENT_SAME_EXCEPTION; +import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_KEYWORD_SEQUENCE_EXCEPTION; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -65,10 +38,8 @@ public class Keyword { @Column(name = "session_id", nullable = false) private Long sessionId; - @ElementCollection - @CollectionTable(name = "keyword_reference") - @Column(name = "url") - private List references; + @OneToMany(mappedBy = "keyword", cascade = CascadeType.ALL, orphanRemoval = true) + private Set recommendedPosts = new HashSet<>(); @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_id") @@ -124,6 +95,10 @@ private void validateKeywordParent(final Keyword parentKeyword) { } } + public boolean isRoot() { + return parent == null; + } + public Long getParentIdOrNull() { if (parent == null) { return null; diff --git a/backend/src/main/java/wooteco/prolog/roadmap/domain/RecommendedPost.java b/backend/src/main/java/wooteco/prolog/roadmap/domain/RecommendedPost.java new file mode 100644 index 000000000..727dca236 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/roadmap/domain/RecommendedPost.java @@ -0,0 +1,87 @@ +package wooteco.prolog.roadmap.domain; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import wooteco.prolog.common.exception.BadRequestException; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import java.util.Objects; + +import static io.micrometer.core.instrument.util.StringUtils.isBlank; +import static java.util.Objects.hash; +import static java.util.Objects.isNull; +import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_KEYWORD_NOT_FOUND_EXCEPTION; +import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_RECOMMENDED_POST_INVALID_URL_LENGTH; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class RecommendedPost { + + public static final int URL_LENGTH_UPPER_BOUND = 512; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String url; + + @ManyToOne + @JoinColumn(nullable = false) + private Keyword keyword; + + public RecommendedPost(final Long id, final String url, final Keyword keyword) { + validate(url, keyword); + + this.id = id; + this.url = url.trim(); + this.keyword = keyword; + } + + private void validate(final String url, final Keyword keyword) { + if (isNull(keyword)) { + throw new BadRequestException(ROADMAP_KEYWORD_NOT_FOUND_EXCEPTION); + } + if (isBlank(url) || url.trim().length() > URL_LENGTH_UPPER_BOUND) { + throw new BadRequestException(ROADMAP_RECOMMENDED_POST_INVALID_URL_LENGTH); + } + } + + public RecommendedPost(final String url, final Keyword keyword) { + this(null, url, keyword); + } + + public void updateUrl(final String url) { + this.url = url; + } + + public void remove() { + keyword.getRecommendedPosts().remove(this); + } + + public void addKeyword(final Keyword keyword) { + this.keyword = keyword; + keyword.getRecommendedPosts().add(this); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof RecommendedPost)) return false; + final RecommendedPost post = (RecommendedPost) o; + return Objects.equals(id, post.id); + } + + @Override + public int hashCode() { + return hash(id); + } +} diff --git a/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/EssayAnswerRepository.java b/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/EssayAnswerRepository.java index 990a12ca2..4379be359 100644 --- a/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/EssayAnswerRepository.java +++ b/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/EssayAnswerRepository.java @@ -4,9 +4,11 @@ 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 wooteco.prolog.roadmap.domain.EssayAnswer; -public interface EssayAnswerRepository extends JpaRepository { +public interface EssayAnswerRepository extends JpaRepository, + JpaSpecificationExecutor { @Query("select ea from EssayAnswer ea where ea.quiz.id = :quizId and ea.member.id = :memberId ") boolean existsByQuizIdAndMemberId(Long quizId, Long memberId); diff --git a/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/EssayAnswerSpecification.java b/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/EssayAnswerSpecification.java new file mode 100644 index 000000000..c939b79f1 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/EssayAnswerSpecification.java @@ -0,0 +1,68 @@ +package wooteco.prolog.roadmap.domain.repository; + +import java.util.List; +import javax.persistence.criteria.JoinType; +import org.springframework.data.jpa.domain.Specification; +import wooteco.prolog.roadmap.domain.EssayAnswer; + +public class EssayAnswerSpecification { + + private EssayAnswerSpecification() { + } + + public static Specification equalsSessionIdsIn(final List sessionIds) { + return (root, query, builder) -> { + if (sessionIds == null || sessionIds.isEmpty()) { + return builder.and(); + } + + return root.join("quiz", JoinType.INNER) + .join("keyword", JoinType.INNER) + .get("sessionId").in(sessionIds); + }; + } + + public static Specification equalsKeywordId(Long keywordId) { + return (root, query, builder) -> { + if (keywordId == null || keywordId == 0L) { + return builder.and(); + } + + return root.get("quiz").get("keyword").get("id").in(keywordId); + }; + } + + public static Specification inQuizIds(List quizIds) { + return (root, query, builder) -> { + if (quizIds == null || quizIds.isEmpty()) { + return builder.and(); + } + + return root.get("quiz").get("id").in(quizIds); + }; + } + + public static Specification inMemberIds(List memberIds) { + return (root, query, builder) -> { + if (memberIds == null || memberIds.isEmpty()) { + return builder.and(); + } + + return root.join("member", JoinType.LEFT).get("id").in(memberIds); + }; + } + + public static Specification orderByIdDesc() { + return (root, query, builder) -> { + query.orderBy(builder.desc(root.get("id"))); + return null; + }; + } + + public static Specification distinct(final boolean distinct) { + return (root, query, builder) -> { + query.distinct(distinct); + return null; + }; + } +} 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 d15d4e7ae..fc0237937 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,21 +1,39 @@ package wooteco.prolog.roadmap.domain.repository; -import java.util.List; -import java.util.Set; +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 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); + + @EntityGraph(attributePaths = "recommendedPosts", type = FETCH) + List findAll(); + @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 c.children lc WHERE k.id = :keywordId ORDER BY k.seq") - Keyword findFetchById(@Param("keywordId") Long keywordId); + + "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 " + + "LEFT JOIN FETCH k.recommendedPosts " + "WHERE k.sessionId = :sessionId AND k.parent IS NULL") List findBySessionIdAndParentIsNull(@Param("sessionId") Long sessionId); diff --git a/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/RecommendedPostRepository.java b/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/RecommendedPostRepository.java new file mode 100644 index 000000000..44540d069 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/RecommendedPostRepository.java @@ -0,0 +1,7 @@ +package wooteco.prolog.roadmap.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import wooteco.prolog.roadmap.domain.RecommendedPost; + +public interface RecommendedPostRepository extends JpaRepository { +} diff --git a/backend/src/main/java/wooteco/prolog/roadmap/ui/EssayAnswerController.java b/backend/src/main/java/wooteco/prolog/roadmap/ui/EssayAnswerController.java index 1aa42fb9a..d90b85fda 100644 --- a/backend/src/main/java/wooteco/prolog/roadmap/ui/EssayAnswerController.java +++ b/backend/src/main/java/wooteco/prolog/roadmap/ui/EssayAnswerController.java @@ -4,6 +4,8 @@ import java.util.List; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -19,9 +21,12 @@ import wooteco.prolog.roadmap.application.QuizService; import wooteco.prolog.roadmap.application.dto.EssayAnswerRequest; import wooteco.prolog.roadmap.application.dto.EssayAnswerResponse; +import wooteco.prolog.roadmap.application.dto.EssayAnswerSearchRequest; import wooteco.prolog.roadmap.application.dto.EssayAnswerUpdateRequest; import wooteco.prolog.roadmap.application.dto.QuizResponse; import wooteco.prolog.roadmap.domain.EssayAnswer; +import wooteco.prolog.studylog.application.dto.EssayAnswersResponse; + @RestController @RequestMapping @@ -44,6 +49,13 @@ public ResponseEntity create(@RequestBody EssayAnswerRequest request, return ResponseEntity.ok(essayAnswerService.createEssayAnswer(request, member.getId())); } + @GetMapping("/essay-answers") + public ResponseEntity search( + EssayAnswerSearchRequest request, + @PageableDefault Pageable pageable) { + return ResponseEntity.ok(essayAnswerService.searchEssayAnswers(request, pageable)); + } + @GetMapping("/essay-answers/{essayAnswerId}") public ResponseEntity findById(@PathVariable Long essayAnswerId) { EssayAnswer essayAnswer = essayAnswerService.getById(essayAnswerId); diff --git a/backend/src/main/java/wooteco/prolog/roadmap/ui/RecommendedController.java b/backend/src/main/java/wooteco/prolog/roadmap/ui/RecommendedController.java new file mode 100644 index 000000000..aa83c816d --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/roadmap/ui/RecommendedController.java @@ -0,0 +1,47 @@ +package wooteco.prolog.roadmap.ui; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import wooteco.prolog.roadmap.application.RecommendedPostService; +import wooteco.prolog.roadmap.application.dto.RecommendedRequest; +import wooteco.prolog.roadmap.application.dto.RecommendedUpdateRequest; + +@RestController +@RequestMapping("/keywords/{keywordId}/recommended-posts") +public class RecommendedController { + + private final RecommendedPostService recommendedPostService; + + public RecommendedController(final RecommendedPostService recommendedPostService) { + this.recommendedPostService = recommendedPostService; + } + + @PostMapping + public ResponseEntity createRecommendedPost(@PathVariable("keywordId") final Long keywordId, + @RequestBody final RecommendedRequest request) { + recommendedPostService.create(keywordId, request); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + @PutMapping("/{recommendedId}") + public ResponseEntity updateRecommendedPost(@PathVariable("keywordId") final Long keywordId, + @PathVariable("recommendedId") final Long recommendedId, + @RequestBody final RecommendedUpdateRequest request) { + recommendedPostService.update(recommendedId, request); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/{recommendedId}") + public ResponseEntity deleteRecommendedPost(@PathVariable("keywordId") final Long keywordId, + @PathVariable("recommendedId") final Long recommendedId) { + recommendedPostService.delete(recommendedId); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/wooteco/prolog/roadmap/ui/RoadmapController.java b/backend/src/main/java/wooteco/prolog/roadmap/ui/RoadmapController.java index 16500a192..4fbfc82ee 100644 --- a/backend/src/main/java/wooteco/prolog/roadmap/ui/RoadmapController.java +++ b/backend/src/main/java/wooteco/prolog/roadmap/ui/RoadmapController.java @@ -14,7 +14,7 @@ public class RoadmapController { private final RoadMapService roadMapService; @GetMapping("/roadmaps") - public KeywordsResponse findKeywords(@RequestParam final Long curriculumId) { + public KeywordsResponse findRoadMapKeyword(@RequestParam final Long curriculumId) { return roadMapService.findAllKeywords(curriculumId); } } diff --git a/backend/src/main/java/wooteco/prolog/session/application/SessionService.java b/backend/src/main/java/wooteco/prolog/session/application/SessionService.java index ae28a2028..7d0592ed9 100644 --- a/backend/src/main/java/wooteco/prolog/session/application/SessionService.java +++ b/backend/src/main/java/wooteco/prolog/session/application/SessionService.java @@ -2,7 +2,7 @@ import static java.util.stream.Collectors.toList; import static wooteco.prolog.common.exception.BadRequestCode.DUPLICATE_SESSION_EXCEPTION; -import static wooteco.prolog.common.exception.BadRequestCode.SESSION_NOT_FOUND_EXCEPTION; +import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_SESSION_NOT_FOUND_EXCEPTION; import java.util.Collection; import java.util.List; @@ -43,7 +43,7 @@ private void validateName(String name) { public Session findById(Long id) { return sessionRepository.findById(id) - .orElseThrow(() -> new BadRequestException(SESSION_NOT_FOUND_EXCEPTION)); + .orElseThrow(() -> new BadRequestException(ROADMAP_SESSION_NOT_FOUND_EXCEPTION)); } public Optional findSessionById(Long id) { diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/dto/EssayAnswersResponse.java b/backend/src/main/java/wooteco/prolog/studylog/application/dto/EssayAnswersResponse.java new file mode 100644 index 000000000..2157e649a --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/studylog/application/dto/EssayAnswersResponse.java @@ -0,0 +1,45 @@ +package wooteco.prolog.studylog.application.dto; + + +import static java.util.stream.Collectors.toList; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import wooteco.prolog.roadmap.application.dto.EssayAnswerResponse; +import wooteco.prolog.roadmap.domain.EssayAnswer; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class EssayAnswersResponse { + + private static final int ONE_INDEXED_PARAMETER = 1; + + private List data; + private Long totalSize; + private int totalPage; + private int currPage; + + public static EssayAnswersResponse of(Page page) { + Page responsePage = new PageImpl<>( + toResponses(page.getContent()), + page.getPageable(), + page.getTotalElements() + ); + + return new EssayAnswersResponse(responsePage.getContent(), + responsePage.getTotalElements(), + responsePage.getTotalPages(), + responsePage.getNumber() + ONE_INDEXED_PARAMETER); + } + + private static List toResponses(List essayAnswers) { + return essayAnswers.stream() + .map(EssayAnswerResponse::of) + .collect(toList()); + } +} diff --git a/backend/src/main/resources/db/migration/prod/V4__alter_table_keyword_reference.sql b/backend/src/main/resources/db/migration/prod/V4__alter_table_keyword_reference.sql new file mode 100644 index 000000000..99090b659 --- /dev/null +++ b/backend/src/main/resources/db/migration/prod/V4__alter_table_keyword_reference.sql @@ -0,0 +1,10 @@ +drop table prolog.keyword_reference; + +create table if not exists prolog.recommended_post +( + id bigint auto_increment primary key, + url varchar(512) not null, + keyword_id bigint not null, + constraint FK_RECOMMENDED_POST_PARENT_KEYWORD_ID + foreign key (keyword_id) references prolog.keyword (id) +); diff --git a/backend/src/test/java/wooteco/prolog/roadmap/application/EssayAnswerSearchTest.java b/backend/src/test/java/wooteco/prolog/roadmap/application/EssayAnswerSearchTest.java new file mode 100644 index 000000000..5a6be087b --- /dev/null +++ b/backend/src/test/java/wooteco/prolog/roadmap/application/EssayAnswerSearchTest.java @@ -0,0 +1,337 @@ +package wooteco.prolog.roadmap.application; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.TestConstructor; +import org.springframework.test.context.TestConstructor.AutowireMode; +import org.springframework.transaction.annotation.Transactional; +import wooteco.prolog.member.application.dto.MemberResponse; +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.EssayAnswerRequest; +import wooteco.prolog.roadmap.application.dto.EssayAnswerResponse; +import wooteco.prolog.roadmap.application.dto.EssayAnswerSearchRequest; +import wooteco.prolog.roadmap.application.dto.QuizResponse; +import wooteco.prolog.roadmap.domain.Curriculum; +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.KeywordRepository; +import wooteco.prolog.roadmap.domain.repository.QuizRepository; +import wooteco.prolog.session.domain.Session; +import wooteco.prolog.session.domain.repository.SessionRepository; +import wooteco.prolog.studylog.application.dto.EssayAnswersResponse; + +@SpringBootTest +@Transactional +@TestConstructor(autowireMode = AutowireMode.ALL) +public class EssayAnswerSearchTest { + + private CurriculumRepository curriculumRepository; + private SessionRepository sessionRepository; + private KeywordRepository keywordRepository; + private QuizRepository quizRepository; + private MemberRepository memberRepository; + private EssayAnswerService essayAnswerService; + + public EssayAnswerSearchTest(CurriculumRepository curriculumRepository, + SessionRepository sessionRepository, + KeywordRepository keywordRepository, + QuizRepository quizRepository, MemberRepository memberRepository, + EssayAnswerService essayAnswerService) { + this.curriculumRepository = curriculumRepository; + this.sessionRepository = sessionRepository; + this.keywordRepository = keywordRepository; + this.quizRepository = quizRepository; + this.memberRepository = memberRepository; + this.essayAnswerService = essayAnswerService; + } + + private Curriculum curriculum; + private Keyword keyword1, keyword2, keyword3, keyword4; + private Quiz quiz1, quiz2, quiz3, quiz4, quiz5, quiz6, quiz7, quiz8, quiz9; + private Member member1, member2, member3, member4; + private Long essayAnswerId1, essayAnswerId2, essayAnswerId3, essayAnswerId4, essayAnswerId5, + essayAnswerId6, essayAnswerId7, essayAnswerId8, essayAnswerId9, essayAnswerId10, + essayAnswerId11, essayAnswerId12, essayAnswerId13, essayAnswerId14, essayAnswerId15, + essayAnswerId16, essayAnswerId17, essayAnswerId18, essayAnswerId19, essayAnswerId20; + + @BeforeEach + void init() { + curriculum = curriculumRepository.save(new Curriculum("커리큘럼1")); + Session session1 = sessionRepository.save(new Session(curriculum.getId(), "세션1")); + Session session2 = sessionRepository.save(new Session(curriculum.getId(), "세션2")); + + keyword1 = keywordRepository.save( + Keyword.createKeyword("자바", "자바 설명", 1, 5, session1.getId(), null)); + keyword2 = keywordRepository.save( + Keyword.createKeyword("키워드", "키워드 설명", 2, 5, session2.getId(), null)); + keyword3 = keywordRepository.save( + Keyword.createKeyword("자바3", "자바 설명3", 3, 5, session1.getId(), null)); + keyword4 = keywordRepository.save( + Keyword.createKeyword("자바4", "자바 설명4", 4, 5, session2.getId(), null)); + + quiz1 = quizRepository.save(new Quiz(keyword1, "퀴즈1")); + quiz2 = quizRepository.save(new Quiz(keyword2, "퀴즈2")); + quiz3 = quizRepository.save(new Quiz(keyword2, "퀴즈3")); + quiz4 = quizRepository.save(new Quiz(keyword3, "퀴즈4")); + quiz5 = quizRepository.save(new Quiz(keyword3, "퀴즈5")); + quiz6 = quizRepository.save(new Quiz(keyword4, "퀴즈6")); + quiz7 = quizRepository.save(new Quiz(keyword4, "퀴즈7")); + quiz8 = quizRepository.save(new Quiz(keyword4, "퀴즈8")); + quiz9 = quizRepository.save(new Quiz(keyword4, "퀴즈9")); + + member1 = memberRepository.save( + new Member("username1", "nickname1", Role.CREW, 111L, "https://")); + member2 = memberRepository.save( + new Member("username2", "nickname2", Role.CREW, 112L, "https://")); + member3 = memberRepository.save( + new Member("username3", "nickname3", Role.CREW, 113L, "https://")); + member4 = memberRepository.save( + new Member("username4", "nickname4", Role.CREW, 115L, "https://")); + + final EssayAnswerRequest essayAnswer1 = new EssayAnswerRequest(quiz1.getId(), "대답1"); + final EssayAnswerRequest essayAnswer2 = new EssayAnswerRequest(quiz2.getId(), "대답2"); + final EssayAnswerRequest essayAnswer3 = new EssayAnswerRequest(quiz2.getId(), "대답3"); + final EssayAnswerRequest essayAnswer4 = new EssayAnswerRequest(quiz2.getId(), "대답4"); + final EssayAnswerRequest essayAnswer5 = new EssayAnswerRequest(quiz2.getId(), "대답5"); + final EssayAnswerRequest essayAnswer6 = new EssayAnswerRequest(quiz3.getId(), "대답6"); + final EssayAnswerRequest essayAnswer7 = new EssayAnswerRequest(quiz4.getId(), "대답7"); + final EssayAnswerRequest essayAnswer8 = new EssayAnswerRequest(quiz4.getId(), "대답8"); + final EssayAnswerRequest essayAnswer9 = new EssayAnswerRequest(quiz5.getId(), "대답9"); + final EssayAnswerRequest essayAnswer10 = new EssayAnswerRequest(quiz5.getId(), "대답10"); + final EssayAnswerRequest essayAnswer11 = new EssayAnswerRequest(quiz6.getId(), "대답11"); + final EssayAnswerRequest essayAnswer12 = new EssayAnswerRequest(quiz6.getId(), "대답12"); + final EssayAnswerRequest essayAnswer13 = new EssayAnswerRequest(quiz6.getId(), "대답13"); + final EssayAnswerRequest essayAnswer14 = new EssayAnswerRequest(quiz7.getId(), "대답14"); + final EssayAnswerRequest essayAnswer15 = new EssayAnswerRequest(quiz7.getId(), "대답15"); + final EssayAnswerRequest essayAnswer16 = new EssayAnswerRequest(quiz8.getId(), "대답16"); + final EssayAnswerRequest essayAnswer17 = new EssayAnswerRequest(quiz8.getId(), "대답17"); + final EssayAnswerRequest essayAnswer18 = new EssayAnswerRequest(quiz9.getId(), "대답18"); + final EssayAnswerRequest essayAnswer19 = new EssayAnswerRequest(quiz9.getId(), "대답19"); + final EssayAnswerRequest essayAnswer20 = new EssayAnswerRequest(quiz9.getId(), "대답20"); + + essayAnswerId1 = essayAnswerService.createEssayAnswer(essayAnswer1, member1.getId()); + essayAnswerId2 = essayAnswerService.createEssayAnswer(essayAnswer2, member1.getId()); + essayAnswerId3 = essayAnswerService.createEssayAnswer(essayAnswer3, member2.getId()); + essayAnswerId4 = essayAnswerService.createEssayAnswer(essayAnswer4, member3.getId()); + essayAnswerId5 = essayAnswerService.createEssayAnswer(essayAnswer5, member4.getId()); + essayAnswerId6 = essayAnswerService.createEssayAnswer(essayAnswer6, member1.getId()); + essayAnswerId7 = essayAnswerService.createEssayAnswer(essayAnswer7, member1.getId()); + essayAnswerId8 = essayAnswerService.createEssayAnswer(essayAnswer8, member2.getId()); + essayAnswerId9 = essayAnswerService.createEssayAnswer(essayAnswer9, member1.getId()); + essayAnswerId10 = essayAnswerService.createEssayAnswer(essayAnswer10, member2.getId()); + essayAnswerId11 = essayAnswerService.createEssayAnswer(essayAnswer11, member1.getId()); + essayAnswerId12 = essayAnswerService.createEssayAnswer(essayAnswer12, member2.getId()); + essayAnswerId13 = essayAnswerService.createEssayAnswer(essayAnswer13, member3.getId()); + essayAnswerId14 = essayAnswerService.createEssayAnswer(essayAnswer14, member1.getId()); + essayAnswerId15 = essayAnswerService.createEssayAnswer(essayAnswer15, member2.getId()); + essayAnswerId16 = essayAnswerService.createEssayAnswer(essayAnswer16, member1.getId()); + essayAnswerId17 = essayAnswerService.createEssayAnswer(essayAnswer17, member2.getId()); + essayAnswerId18 = essayAnswerService.createEssayAnswer(essayAnswer18, member1.getId()); + essayAnswerId19 = essayAnswerService.createEssayAnswer(essayAnswer19, member2.getId()); + essayAnswerId20 = essayAnswerService.createEssayAnswer(essayAnswer20, member3.getId()); + } + + @Test + @DisplayName("커리큘럼 아이디 O, 키워드 아이디 X, 퀴즈 아이디 X, 멤버 아이디 X") + void 커리큘럼_아이디를_받아서_답변을_검색한다() { + // given + final EssayAnswerSearchRequest 답변_검색_요청 = new EssayAnswerSearchRequest( + curriculum.getId(), null, null, null); + + // when + final EssayAnswersResponse 답변_응답들 = essayAnswerService.searchEssayAnswers( + 답변_검색_요청, PageRequest.of(0, 10)); + + // then + final List 예상_답변_응답_리스트 = Arrays.asList( + 예상_답변_응답(essayAnswerId20, quiz9, "대답20", member3), + 예상_답변_응답(essayAnswerId19, quiz9, "대답19", member2), + 예상_답변_응답(essayAnswerId18, quiz9, "대답18", member1), + 예상_답변_응답(essayAnswerId17, quiz8, "대답17", member2), + 예상_답변_응답(essayAnswerId16, quiz8, "대답16", member1), + 예상_답변_응답(essayAnswerId15, quiz7, "대답15", member2), + 예상_답변_응답(essayAnswerId14, quiz7, "대답14", member1), + 예상_답변_응답(essayAnswerId13, quiz6, "대답13", member3), + 예상_답변_응답(essayAnswerId12, quiz6, "대답12", member2), + 예상_답변_응답(essayAnswerId11, quiz6, "대답11", member1) + ); + final EssayAnswersResponse 예상되는_답변_응답들 = new EssayAnswersResponse(예상_답변_응답_리스트, 20L, 2, 1); + + assertThat(답변_응답들) + .usingRecursiveComparison() + .ignoringFields("data.createdAt", "data.updatedAt") + .isEqualTo(예상되는_답변_응답들); + } + + @Test + @DisplayName("커리큘럼 아이디 O, 키워드 아이디 O, 퀴즈 아이디 X, 멤버 아이디 X") + void 커리큘럼_아이디와_키워드_아이디를_받아서_답변을_검색한다() { + // given + final EssayAnswerSearchRequest 답변_검색_요청 = new EssayAnswerSearchRequest( + curriculum.getId(), keyword3.getId(), null, null); + + // when + final EssayAnswersResponse 답변_응답들 = essayAnswerService.searchEssayAnswers( + 답변_검색_요청, PageRequest.of(0, 10)); + + // then + final List 예상_답변_응답_리스트 = Arrays.asList( + 예상_답변_응답(essayAnswerId10, quiz5, "대답10", member2), + 예상_답변_응답(essayAnswerId9, quiz5, "대답9", member1), + 예상_답변_응답(essayAnswerId8, quiz4, "대답8", member2), + 예상_답변_응답(essayAnswerId7, quiz4, "대답7", member1) + ); + + final EssayAnswersResponse 예상되는_답변_응답들 = new EssayAnswersResponse(예상_답변_응답_리스트, 4L, 1, 1); + + assertThat(답변_응답들) + .usingRecursiveComparison() + .ignoringFields("data.createdAt", "data.updatedAt") + .isEqualTo(예상되는_답변_응답들); + } + + @Test + @DisplayName("커리큘럼 아이디 O, 키워드 아이디 O, 퀴즈 아이디 O, 멤버 아이디 X") + void 커리큘럼_아이디와_키워드_아이디와_퀴즈_아이디를_받아서_답변을_검색한다() { + // given + final EssayAnswerSearchRequest 답변_검색_요청 = new EssayAnswerSearchRequest( + curriculum.getId(), keyword4.getId(), + Arrays.asList(quiz6.getId(), quiz9.getId()), null); + + // when + final EssayAnswersResponse 답변_응답들 = essayAnswerService.searchEssayAnswers( + 답변_검색_요청, PageRequest.of(0, 10)); + + // then + final List 예상_답변_응답_리스트 = Arrays.asList( + 예상_답변_응답(essayAnswerId20, quiz9, "대답20", member3), + 예상_답변_응답(essayAnswerId19, quiz9, "대답19", member2), + 예상_답변_응답(essayAnswerId18, quiz9, "대답18", member1), + 예상_답변_응답(essayAnswerId13, quiz6, "대답13", member3), + 예상_답변_응답(essayAnswerId12, quiz6, "대답12", member2), + 예상_답변_응답(essayAnswerId11, quiz6, "대답11", member1) + ); + + final EssayAnswersResponse 예상되는_답변_응답들 = new EssayAnswersResponse(예상_답변_응답_리스트, 6L, 1, 1); + + assertThat(답변_응답들) + .usingRecursiveComparison() + .ignoringFields("data.createdAt", "data.updatedAt") + .isEqualTo(예상되는_답변_응답들); + } + + @Test + @DisplayName("커리큘럼 아이디 O, 키워드 아이디 O, 퀴즈 아이디 X, 멤버 아이디 O") + void 커리큘럼_아이디와_키워드_아이디와_멤버_아이디를_받아서_답변을_검색한다() { + // given + final EssayAnswerSearchRequest request = new EssayAnswerSearchRequest( + curriculum.getId(), + keyword4.getId(), + null, + Arrays.asList(member1.getId()) + ); + + // when + final EssayAnswersResponse actual = essayAnswerService.searchEssayAnswers( + request, PageRequest.of(0, 10)); + + // then + final List expected = Arrays.asList( + 예상_답변_응답(essayAnswerId18, quiz9, "대답18", member1), + 예상_답변_응답(essayAnswerId16, quiz8, "대답16", member1), + 예상_답변_응답(essayAnswerId14, quiz7, "대답14", member1), + 예상_답변_응답(essayAnswerId11, quiz6, "대답11", member1) + ); + + final EssayAnswersResponse response = new EssayAnswersResponse(expected, 4L, 1, 1); + + assertThat(actual) + .usingRecursiveComparison() + .ignoringFields("data.createdAt", "data.updatedAt") + .isEqualTo(response); + } + + @Test + @DisplayName("커리큘럼 아이디 O, 키워드 아이디 O, 퀴즈 아이디 O, 멤버 아이디 O") + void 커리큘럼_아이디와_키워드_아이디와_퀴즈_아이디와_멤버_아이디를_받아서_답변을_검색한다() { + // given + final EssayAnswerSearchRequest request = new EssayAnswerSearchRequest( + curriculum.getId(), + keyword4.getId(), + Arrays.asList(quiz6.getId(), quiz7.getId()), + Arrays.asList(member1.getId()) + ); + + // when + final EssayAnswersResponse actual = essayAnswerService.searchEssayAnswers( + request, PageRequest.of(0, 10)); + + // then + final List expected = Arrays.asList( + 예상_답변_응답(essayAnswerId14, quiz7, "대답14", member1), + 예상_답변_응답(essayAnswerId11, quiz6, "대답11", member1) + ); + + final EssayAnswersResponse response = new EssayAnswersResponse(expected, 2L, 1, 1); + + assertThat(actual) + .usingRecursiveComparison() + .ignoringFields("data.createdAt", "data.updatedAt") + .isEqualTo(response); + } + + @Test + @DisplayName("커리큘럼 아이디 O, 키워드 아이디 X, 퀴즈 아이디 X, 멤버 아이디 O") + void 커리큘럼_아이디와_멤버_아이디를_받아서_답변을_검색한다() { + // given + final EssayAnswerSearchRequest request = new EssayAnswerSearchRequest( + curriculum.getId(), + null, + null, + Arrays.asList(member1.getId()) + ); + + // when + final EssayAnswersResponse actual = essayAnswerService.searchEssayAnswers( + request, PageRequest.of(0, 10)); + + // then + final List expected = Arrays.asList( + 예상_답변_응답(essayAnswerId18, quiz9, "대답18", member1), + 예상_답변_응답(essayAnswerId16, quiz8, "대답16", member1), + 예상_답변_응답(essayAnswerId14, quiz7, "대답14", member1), + 예상_답변_응답(essayAnswerId11, quiz6, "대답11", member1), + 예상_답변_응답(essayAnswerId9, quiz5, "대답9", member1), + 예상_답변_응답(essayAnswerId7, quiz4, "대답7", member1), + 예상_답변_응답(essayAnswerId6, quiz3, "대답6", member1), + 예상_답변_응답(essayAnswerId2, quiz2, "대답2", member1), + 예상_답변_응답(essayAnswerId1, quiz1, "대답1", member1) + ); + + final EssayAnswersResponse response = new EssayAnswersResponse(expected, 9L, 1, 1); + + assertThat(actual) + .usingRecursiveComparison() + .ignoringFields("data.createdAt", "data.updatedAt") + .isEqualTo(response); + } + + private EssayAnswerResponse 예상_답변_응답(final Long essayAnswerId, final Quiz quiz, + final String answer, final Member member) { + return new EssayAnswerResponse(essayAnswerId, + new QuizResponse(quiz.getId(), quiz.getQuestion()), + answer, new MemberResponse(member.getId(), member.getUsername(), member.getNickname(), + Role.CREW, "https://"), LocalDateTime.now(), LocalDateTime.now()); + } +} diff --git a/backend/src/test/java/wooteco/prolog/roadmap/application/KeywordServiceTest.java b/backend/src/test/java/wooteco/prolog/roadmap/application/KeywordServiceTest.java index 36477238a..2bb5f86ec 100644 --- a/backend/src/test/java/wooteco/prolog/roadmap/application/KeywordServiceTest.java +++ b/backend/src/test/java/wooteco/prolog/roadmap/application/KeywordServiceTest.java @@ -1,14 +1,5 @@ package wooteco.prolog.roadmap.application; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_KEYWORD_NOT_FOUND_EXCEPTION; - -import java.util.Collections; -import java.util.Optional; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -22,6 +13,13 @@ import wooteco.prolog.roadmap.domain.repository.KeywordRepository; import wooteco.prolog.session.domain.repository.SessionRepository; +import java.util.Collections; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; +import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_KEYWORD_NOT_FOUND_EXCEPTION; + @ExtendWith(MockitoExtension.class) class KeywordServiceTest { @@ -105,13 +103,13 @@ void findKeywordWithAllChild() { when(keywordRepository.existsById(any())).thenReturn(true); Keyword keyword = new Keyword(1L, "", "", 1, 1, 1L, null, Collections.emptySet()); - when(keywordRepository.findFetchById(1L)).thenReturn(keyword); + when(keywordRepository.findFetchByIdOrderBySeq(1L)).thenReturn(keyword); //when keywordService.findKeywordWithAllChild(1L, 1L); //then - verify(keywordRepository, times(1)).findFetchById(any()); + verify(keywordRepository, times(1)).findFetchByIdOrderBySeq(any()); } @DisplayName("sessionId로 최상위 키워드들을 찾을 수 있다") @@ -178,7 +176,7 @@ void deleteKeyword() { keywordService.deleteKeyword(1L, 1L); //then - verify(keywordRepository, times(1)).findFetchById(any()); + verify(keywordRepository, times(1)).findFetchByIdOrderBySeq(any()); verify(keywordRepository, times(1)).delete(any()); } } diff --git a/backend/src/test/java/wooteco/prolog/roadmap/application/RecommendedPostServiceTest.java b/backend/src/test/java/wooteco/prolog/roadmap/application/RecommendedPostServiceTest.java new file mode 100644 index 000000000..5f3932c58 --- /dev/null +++ b/backend/src/test/java/wooteco/prolog/roadmap/application/RecommendedPostServiceTest.java @@ -0,0 +1,99 @@ +package wooteco.prolog.roadmap.application; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import wooteco.prolog.common.DataInitializer; +import wooteco.prolog.roadmap.application.dto.RecommendedRequest; +import wooteco.prolog.roadmap.application.dto.RecommendedUpdateRequest; +import wooteco.prolog.roadmap.domain.Keyword; +import wooteco.prolog.roadmap.domain.RecommendedPost; +import wooteco.prolog.roadmap.domain.repository.KeywordRepository; +import wooteco.prolog.roadmap.domain.repository.RecommendedPostRepository; +import wooteco.prolog.session.domain.Session; +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 { + + @Autowired + private RecommendedPostService recommendedPostService; + @Autowired + private RecommendedPostRepository recommendedPostRepository; + @Autowired + private KeywordRepository keywordRepository; + @Autowired + private SessionRepository sessionRepository; + + @Autowired + private DataInitializer dataInitializer; + + private Keyword keyword; + + @BeforeEach + public void init() { + final Session session = sessionRepository.save(new Session("레벨 1")); + this.keyword = keywordRepository.save(Keyword.createKeyword("이름", "설명", 1, 1, session.getId(), null)); + } + + @AfterEach + public void removeAll() { + dataInitializer.execute(); + } + + @Test + @DisplayName("추천 포스트 생성 테스트") + void create() { + //given + final RecommendedRequest request = new RecommendedRequest("https://example.com"); + + //when + recommendedPostService.create(keyword.getId(), request); + + //then + assertThat(recommendedPostRepository.findAll()).hasSize(1); + } + + @Test + @DisplayName("추천 포스트 수정 테스트") + void update() { + //given + final Long recommendedPostId = recommendedPostService.create( + keyword.getId(), + new RecommendedRequest("https://example.com")); + + final String newUrl = "https://new.com"; + final RecommendedUpdateRequest updateRequest = new RecommendedUpdateRequest(newUrl); + + //when + recommendedPostService.update(recommendedPostId, updateRequest); + + //then + final RecommendedPost post = recommendedPostRepository.findById(recommendedPostId).get(); + assertThat(post.getUrl()).isEqualTo(newUrl); + } + + @Test + @DisplayName("추천 포스트 삭제 테스트") + void delete() { + //given + final RecommendedRequest request = new RecommendedRequest("https://example.com"); + final Long recommendedPostId = recommendedPostService.create(keyword.getId(), request); + + //when + recommendedPostService.delete(recommendedPostId); + + //then + assertSoftly(softAssertions -> { + assertThat(recommendedPostRepository.findAll()).hasSize(0); + assertThat(keywordRepository.findById(keyword.getId()).get().getRecommendedPosts()) + .isEmpty(); + }); + } +} 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 469f81277..2fb659eb7 100644 --- a/backend/src/test/java/wooteco/prolog/roadmap/application/RoadMapServiceTest.java +++ b/backend/src/test/java/wooteco/prolog/roadmap/application/RoadMapServiceTest.java @@ -6,6 +6,7 @@ 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; @@ -36,13 +37,13 @@ class RoadMapServiceTest { @Test @DisplayName("curriculumId가 주어지면 해당 커리큘럼의 키워드들을 전부 조회할 수 있다.") - void findAllKeywords() throws Exception { + void findAllKeywords() { //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 = Keyword.createKeyword("자바1", "자바 설명1", 1, 5, session.getId(), - null); + final Keyword keyword = new Keyword(1L, "자바1", "자바 설명1", 1, 5, session.getId(), + null, Collections.emptySet()); when(curriculumRepository.findById(anyLong())) .thenReturn(Optional.of(curriculum)); @@ -53,7 +54,7 @@ void findAllKeywords() throws Exception { when(keywordRepository.findBySessionIdIn(any())) .thenReturn(Arrays.asList(keyword)); - final KeywordsResponse expected = KeywordsResponse.createResponse(Arrays.asList(keyword)); + final KeywordsResponse expected = KeywordsResponse.createResponseWithChildren(Arrays.asList(keyword)); //when final KeywordsResponse actual = diff --git a/backend/src/test/java/wooteco/prolog/roadmap/domain/RecommendedPostTest.java b/backend/src/test/java/wooteco/prolog/roadmap/domain/RecommendedPostTest.java new file mode 100644 index 000000000..b9c234d9b --- /dev/null +++ b/backend/src/test/java/wooteco/prolog/roadmap/domain/RecommendedPostTest.java @@ -0,0 +1,106 @@ +package wooteco.prolog.roadmap.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import wooteco.prolog.common.exception.BadRequestException; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_KEYWORD_NOT_FOUND_EXCEPTION; +import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_RECOMMENDED_POST_INVALID_URL_LENGTH; + +class RecommendedPostTest { + + @Test + @DisplayName("추천 포스트 생성 시 키워드가 null이면 예외가 발생한다") + void construct_fail1() { + assertThatThrownBy(() -> new RecommendedPost("https://example.com", null)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ROADMAP_KEYWORD_NOT_FOUND_EXCEPTION.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + @DisplayName("추천 포스트 생성 시 url이 null이면 예외가 발생한다") + void construct_fail2(final String url) { + assertThatThrownBy(() -> new RecommendedPost(url, null)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ROADMAP_KEYWORD_NOT_FOUND_EXCEPTION.getMessage()); + } + + @Test + @DisplayName("추천 포스트 생성 시 url의 길이가 공백 제외 0이면 예외가 발생한다") + void construct_fail3() { + //given + final String url = " "; + + //when, then + assertThatThrownBy(() -> new RecommendedPost(url, null)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ROADMAP_KEYWORD_NOT_FOUND_EXCEPTION.getMessage()); + } + + @Test + @DisplayName("추천 포스트 생성 시 url의 길이가 공백 제외 512보다 크면 예외가 발생한다") + void construct_fail4() { + //given + final Keyword keyword = Keyword.createKeyword("name", "description", 1, 1, 1L, null); + final String url = Stream.generate(() -> "a") + .limit(513) + .collect(Collectors.joining()); + + //when, then + assertThatThrownBy(() -> new RecommendedPost(url, keyword)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ROADMAP_RECOMMENDED_POST_INVALID_URL_LENGTH.getMessage()); + } + + @Test + @DisplayName("추천 포스트 생성 테스트") + void construct() { + //given + final Keyword keyword = Keyword.createKeyword("name", "description", 1, 1, 1L, null); + final String url = "http://www.salmon"; + + //when, then + assertDoesNotThrow(() -> new RecommendedPost(url, keyword)); + } + + @Test + @DisplayName("삭제 기능 테스트") + void remove() { + //given + final Keyword keyword = Keyword.createKeyword("이름", "설명", 1, 1, 1L, null); + final RecommendedPost recommendedPost = new RecommendedPost("https://example.com", keyword); + + //when + recommendedPost.remove(); + + //then + assertThat(keyword.getRecommendedPosts()).isEmpty(); + } + + @Test + @DisplayName("소속 키워드를 추가한다") + void addKeyword() { + //given + final Keyword keyword = Keyword.createKeyword("name", "description", 1, 1, 1L, null); + final RecommendedPost post = new RecommendedPost("http://연어", keyword); + + //when + post.addKeyword(keyword); + + //then + assertSoftly(soft -> { + assertThat(post.getKeyword()).isEqualTo(keyword); + assertThat(keyword.getRecommendedPosts()).containsExactly(post); + }); + } +} 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 70255a8ba..1b4236b27 100644 --- a/backend/src/test/java/wooteco/prolog/roadmap/repository/KeywordRepositoryTest.java +++ b/backend/src/test/java/wooteco/prolog/roadmap/repository/KeywordRepositoryTest.java @@ -47,7 +47,7 @@ class KeywordRepositoryTest { em.clear(); // when - Keyword extract = keywordRepository.findFetchById(keywordParentId); + Keyword extract = keywordRepository.findFetchByIdOrderBySeq(keywordParentId); // then assertAll( @@ -79,7 +79,7 @@ class KeywordRepositoryTest { em.clear(); // when - Keyword extract = keywordRepository.findFetchById(keywordParentId); + Keyword extract = keywordRepository.findFetchByIdOrderBySeq(keywordParentId); // then assertAll( diff --git a/backend/src/test/java/wooteco/prolog/session/application/SessionServiceTest.java b/backend/src/test/java/wooteco/prolog/session/application/SessionServiceTest.java index f949e3e0f..dd71e8d90 100644 --- a/backend/src/test/java/wooteco/prolog/session/application/SessionServiceTest.java +++ b/backend/src/test/java/wooteco/prolog/session/application/SessionServiceTest.java @@ -1,5 +1,19 @@ package wooteco.prolog.session.application; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.doReturn; +import static wooteco.prolog.common.exception.BadRequestCode.DUPLICATE_SESSION_EXCEPTION; +import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_SESSION_NOT_FOUND_EXCEPTION; +import static wooteco.prolog.login.ui.LoginMember.Authority.ANONYMOUS; +import static wooteco.prolog.login.ui.LoginMember.Authority.MEMBER; + +import java.util.ArrayList; +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; @@ -16,22 +30,6 @@ import wooteco.prolog.session.domain.SessionMember; import wooteco.prolog.session.domain.repository.SessionRepository; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.doReturn; -import static wooteco.prolog.common.exception.BadRequestCode.DUPLICATE_SESSION_EXCEPTION; -import static wooteco.prolog.common.exception.BadRequestCode.SESSION_NOT_FOUND_EXCEPTION; -import static wooteco.prolog.login.ui.LoginMember.Authority.ANONYMOUS; -import static wooteco.prolog.login.ui.LoginMember.Authority.MEMBER; - @ExtendWith(MockitoExtension.class) class SessionServiceTest { @@ -91,7 +89,7 @@ void findByIdFail() { // when, then assertThatThrownBy(() -> sessionService.findById(1L)) .isInstanceOf(BadRequestException.class) - .hasMessage(SESSION_NOT_FOUND_EXCEPTION.getMessage()); + .hasMessage(ROADMAP_SESSION_NOT_FOUND_EXCEPTION.getMessage()); } @DisplayName("Id로 Optional을 조회한다.") diff --git a/backend/src/test/java/wooteco/prolog/session/domain/repository/SessionMemberRepositoryTest.java b/backend/src/test/java/wooteco/prolog/session/domain/repository/SessionMemberRepositoryTest.java index 76cf061d9..60a4e24ec 100644 --- a/backend/src/test/java/wooteco/prolog/session/domain/repository/SessionMemberRepositoryTest.java +++ b/backend/src/test/java/wooteco/prolog/session/domain/repository/SessionMemberRepositoryTest.java @@ -1,7 +1,7 @@ package wooteco.prolog.session.domain.repository; import static org.assertj.core.api.Assertions.assertThat; -import static wooteco.prolog.common.exception.BadRequestCode.SESSION_NOT_FOUND_EXCEPTION; +import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_SESSION_NOT_FOUND_EXCEPTION; import java.util.Optional; import org.junit.jupiter.api.DisplayName; @@ -43,7 +43,7 @@ void findSessionMemberBySessionIdAndMemberId() { // then assertThat(result.isPresent()).isTrue(); assertThat(result.orElseThrow( - () -> new BadRequestException(SESSION_NOT_FOUND_EXCEPTION))).isEqualTo(현구막_백엔드_레벨1); + () -> new BadRequestException(ROADMAP_SESSION_NOT_FOUND_EXCEPTION))).isEqualTo(현구막_백엔드_레벨1); } private Session 강의_생성(String name) {