diff --git a/backend/src/main/java/reviewme/question/repository/QuestionRepository.java b/backend/src/main/java/reviewme/question/repository/QuestionRepository.java index fdeaea795..889202bf3 100644 --- a/backend/src/main/java/reviewme/question/repository/QuestionRepository.java +++ b/backend/src/main/java/reviewme/question/repository/QuestionRepository.java @@ -4,8 +4,11 @@ import java.util.Set; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import reviewme.question.domain.OptionItem; import reviewme.question.domain.Question; +@Repository public interface QuestionRepository extends JpaRepository { @Query(value = """ @@ -27,4 +30,20 @@ public interface QuestionRepository extends JpaRepository { WHERE ts.template_id = :templateId """, nativeQuery = true) List findAllByTemplatedId(long templateId); + + @Query(value = """ + SELECT q FROM Question q + JOIN SectionQuestion sq ON q.id = sq.questionId + WHERE sq.sectionId = :sectionId + ORDER BY q.position + """) + List findAllBySectionIdOrderByPosition(long sectionId); + + @Query(""" + SELECT o FROM OptionItem o + JOIN OptionGroup og ON o.optionGroupId = og.id + WHERE og.questionId = :questionId + ORDER BY o.position + """) + List findAllOptionItemsByIdOrderByPosition(long questionId); } diff --git a/backend/src/main/java/reviewme/review/controller/ReviewController.java b/backend/src/main/java/reviewme/review/controller/ReviewController.java index 7bca618ce..01e091756 100644 --- a/backend/src/main/java/reviewme/review/controller/ReviewController.java +++ b/backend/src/main/java/reviewme/review/controller/ReviewController.java @@ -11,7 +11,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.SessionAttribute; -import reviewme.review.service.GatheredReviewLookupService; +import reviewme.review.service.ReviewGatheredLookupService; import reviewme.review.service.ReviewDetailLookupService; import reviewme.review.service.ReviewListLookupService; import reviewme.review.service.ReviewRegisterService; @@ -30,7 +30,7 @@ public class ReviewController { private final ReviewListLookupService reviewListLookupService; private final ReviewDetailLookupService reviewDetailLookupService; private final ReviewSummaryService reviewSummaryService; - private final GatheredReviewLookupService gatheredReviewLookupService; + private final ReviewGatheredLookupService reviewGatheredLookupService; @PostMapping("/v2/reviews") public ResponseEntity createReview(@Valid @RequestBody ReviewRegisterRequest request) { @@ -68,10 +68,10 @@ public ResponseEntity findReceivedReviewOverview @GetMapping("/v2/reviews/gather") public ResponseEntity getReceivedReviewsBySectionId( - @RequestParam("sectionId") Long sectionId, + @RequestParam("sectionId") long sectionId, @SessionAttribute("reviewRequestCode") String reviewRequestCode ) { - ReviewsGatheredBySectionResponse response = gatheredReviewLookupService.getReceivedReviewsBySectionId( + ReviewsGatheredBySectionResponse response = reviewGatheredLookupService.getReceivedReviewsBySectionId( reviewRequestCode, sectionId); return ResponseEntity.ok(response); } diff --git a/backend/src/main/java/reviewme/review/repository/AnswerRepository.java b/backend/src/main/java/reviewme/review/repository/AnswerRepository.java index aa4814978..821fbb7cc 100644 --- a/backend/src/main/java/reviewme/review/repository/AnswerRepository.java +++ b/backend/src/main/java/reviewme/review/repository/AnswerRepository.java @@ -1,5 +1,7 @@ package reviewme.review.repository; +import java.util.Collection; +import java.util.List; import java.util.Set; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -9,6 +11,15 @@ @Repository public interface AnswerRepository extends JpaRepository { + @Query(value = """ + SELECT a FROM Answer a + JOIN Review r ON a.reviewId = r.id + WHERE r.reviewGroupId = :reviewGroupId AND a.questionId IN :questionIds + ORDER BY r.createdAt DESC + LIMIT :limit + """) + List findReceivedAnswersByQuestionIds(long reviewGroupId, Collection questionIds, int limit); + @Query(value = """ SELECT a.id FROM Answer a JOIN Review r @@ -19,17 +30,15 @@ public interface AnswerRepository extends JpaRepository { @Query(value = """ SELECT a FROM Answer a - JOIN Review r - ON a.reviewId = r.id - WHERE r.reviewGroupId = :reviewGroupId + JOIN Review r + ON a.reviewId = r.id + WHERE r.reviewGroupId = :reviewGroupId """) Set findAllByReviewGroupId(long reviewGroupId); @Query(value = """ SELECT a.id FROM Answer a - JOIN Question q - ON a.questionId = q.id - WHERE q.id = :questionId + WHERE a.questionId = :questionId """) Set findIdsByQuestionId(long questionId); } diff --git a/backend/src/main/java/reviewme/review/service/GatheredReviewLookupService.java b/backend/src/main/java/reviewme/review/service/GatheredReviewLookupService.java deleted file mode 100644 index a48e59525..000000000 --- a/backend/src/main/java/reviewme/review/service/GatheredReviewLookupService.java +++ /dev/null @@ -1,14 +0,0 @@ -package reviewme.review.service; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import reviewme.review.service.dto.response.gathered.ReviewsGatheredBySectionResponse; - -@Service -@RequiredArgsConstructor -public class GatheredReviewLookupService { - - public ReviewsGatheredBySectionResponse getReceivedReviewsBySectionId(String reviewRequestCode, long sectionId) { - return null; - } -} diff --git a/backend/src/main/java/reviewme/review/service/ReviewGatheredLookupService.java b/backend/src/main/java/reviewme/review/service/ReviewGatheredLookupService.java new file mode 100644 index 000000000..86bb7d728 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/ReviewGatheredLookupService.java @@ -0,0 +1,73 @@ +package reviewme.review.service; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import reviewme.question.domain.Question; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.Answer; +import reviewme.review.repository.AnswerRepository; +import reviewme.review.service.dto.response.gathered.ReviewsGatheredBySectionResponse; +import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.review.service.exception.SectionNotFoundInTemplateException; +import reviewme.review.service.mapper.ReviewGatherMapper; +import reviewme.reviewgroup.domain.ReviewGroup; +import reviewme.reviewgroup.repository.ReviewGroupRepository; +import reviewme.template.domain.Section; +import reviewme.template.repository.SectionRepository; + +@Service +@RequiredArgsConstructor +public class ReviewGatheredLookupService { + + private static final int ANSWER_RESPONSE_LIMIT = 100; + + private final QuestionRepository questionRepository; + private final AnswerRepository answerRepository; + private final ReviewGroupRepository reviewGroupRepository; + private final SectionRepository sectionRepository; + + private final ReviewGatherMapper reviewGatherMapper; + + @Transactional(readOnly = true) + public ReviewsGatheredBySectionResponse getReceivedReviewsBySectionId(String reviewRequestCode, long sectionId) { + ReviewGroup reviewGroup = getReviewGroupOrThrow(reviewRequestCode); + Section section = getSectionOrThrow(sectionId, reviewGroup); + Map> questionAnswers = getQuestionAnswers(section, reviewGroup); + + return reviewGatherMapper.mapToReviewsGatheredBySection(questionAnswers); + } + + private ReviewGroup getReviewGroupOrThrow(String reviewRequestCode) { + return reviewGroupRepository.findByReviewRequestCode(reviewRequestCode) + .orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode)); + } + + private Section getSectionOrThrow(long sectionId, ReviewGroup reviewGroup) { + return sectionRepository.findByIdAndTemplateId(sectionId, reviewGroup.getTemplateId()) + .orElseThrow(() -> new SectionNotFoundInTemplateException(sectionId, reviewGroup.getTemplateId())); + } + + private Map> getQuestionAnswers(Section section, ReviewGroup reviewGroup) { + Map questionIdQuestion = questionRepository + .findAllBySectionIdOrderByPosition(section.getId()) + .stream() + .collect(Collectors.toMap(Question::getId, Function.identity())); + + Map> questionIdAnswers = answerRepository + .findReceivedAnswersByQuestionIds(reviewGroup.getId(), questionIdQuestion.keySet(), + ANSWER_RESPONSE_LIMIT) + .stream() + .collect(Collectors.groupingBy(Answer::getQuestionId)); + + return questionIdQuestion.values().stream() + .collect(Collectors.toMap( + Function.identity(), + question -> questionIdAnswers.getOrDefault(question.getId(), List.of()) + )); + } +} diff --git a/backend/src/main/java/reviewme/review/service/dto/response/gathered/AnswerContentResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/gathered/AnswerContentResponse.java deleted file mode 100644 index df73eb56c..000000000 --- a/backend/src/main/java/reviewme/review/service/dto/response/gathered/AnswerContentResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package reviewme.review.service.dto.response.gathered; - -public record AnswerContentResponse( - String content -) { -} diff --git a/backend/src/main/java/reviewme/review/service/dto/response/gathered/HighlightResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/gathered/HighlightResponse.java new file mode 100644 index 000000000..402cc9b09 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/dto/response/gathered/HighlightResponse.java @@ -0,0 +1,9 @@ +package reviewme.review.service.dto.response.gathered; + +import java.util.List; + +public record HighlightResponse( + long lineIndex, + List ranges +) { +} diff --git a/backend/src/main/java/reviewme/review/service/dto/response/gathered/RangeResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/gathered/RangeResponse.java new file mode 100644 index 000000000..046b02f73 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/dto/response/gathered/RangeResponse.java @@ -0,0 +1,7 @@ +package reviewme.review.service.dto.response.gathered; + +public record RangeResponse( + long startIndex, + long endIndex +) { +} diff --git a/backend/src/main/java/reviewme/review/service/dto/response/gathered/ReviewsGatheredByQuestionResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/gathered/ReviewsGatheredByQuestionResponse.java index 9658e80d9..426049908 100644 --- a/backend/src/main/java/reviewme/review/service/dto/response/gathered/ReviewsGatheredByQuestionResponse.java +++ b/backend/src/main/java/reviewme/review/service/dto/response/gathered/ReviewsGatheredByQuestionResponse.java @@ -7,7 +7,7 @@ public record ReviewsGatheredByQuestionResponse( SimpleQuestionResponse question, @Nullable - List answers, + List answers, @Nullable List votes diff --git a/backend/src/main/java/reviewme/review/service/dto/response/gathered/SimpleQuestionResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/gathered/SimpleQuestionResponse.java index 1c9618a72..e16df25e6 100644 --- a/backend/src/main/java/reviewme/review/service/dto/response/gathered/SimpleQuestionResponse.java +++ b/backend/src/main/java/reviewme/review/service/dto/response/gathered/SimpleQuestionResponse.java @@ -3,6 +3,7 @@ import reviewme.question.domain.QuestionType; public record SimpleQuestionResponse( + long id, String name, QuestionType type ) { diff --git a/backend/src/main/java/reviewme/review/service/dto/response/gathered/TextResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/gathered/TextResponse.java new file mode 100644 index 000000000..3684f09e1 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/dto/response/gathered/TextResponse.java @@ -0,0 +1,10 @@ +package reviewme.review.service.dto.response.gathered; + +import java.util.List; + +public record TextResponse( + long id, + String content, + List highlights +) { +} diff --git a/backend/src/main/java/reviewme/review/service/dto/response/gathered/VoteResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/gathered/VoteResponse.java index 57ba21e0d..a2dc887ca 100644 --- a/backend/src/main/java/reviewme/review/service/dto/response/gathered/VoteResponse.java +++ b/backend/src/main/java/reviewme/review/service/dto/response/gathered/VoteResponse.java @@ -2,6 +2,6 @@ public record VoteResponse( String content, - int count + long count ) { } diff --git a/backend/src/main/java/reviewme/review/service/exception/GatheredAnswersTypeNonUniformException.java b/backend/src/main/java/reviewme/review/service/exception/GatheredAnswersTypeNonUniformException.java new file mode 100644 index 000000000..3d13fd987 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/exception/GatheredAnswersTypeNonUniformException.java @@ -0,0 +1,13 @@ +package reviewme.review.service.exception; + +import lombok.extern.slf4j.Slf4j; +import reviewme.global.exception.DataInconsistencyException; + +@Slf4j +public class GatheredAnswersTypeNonUniformException extends DataInconsistencyException { + + public GatheredAnswersTypeNonUniformException(Throwable cause) { + super("서버 내부 오류가 발생했습니다."); + log.error("The types of answers to questions are not uniform.", cause); + } +} diff --git a/backend/src/main/java/reviewme/review/service/exception/SectionNotFoundInTemplateException.java b/backend/src/main/java/reviewme/review/service/exception/SectionNotFoundInTemplateException.java new file mode 100644 index 000000000..9941c8c8a --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/exception/SectionNotFoundInTemplateException.java @@ -0,0 +1,13 @@ +package reviewme.review.service.exception; + +import lombok.extern.slf4j.Slf4j; +import reviewme.global.exception.NotFoundException; + +@Slf4j +public class SectionNotFoundInTemplateException extends NotFoundException { + + public SectionNotFoundInTemplateException(long sectionId, long templateId) { + super("섹션 정보를 찾을 수 없습니다."); + log.info("Section not found in template - sectionId: {}, templateId: {}", sectionId, templateId, this); + } +} diff --git a/backend/src/main/java/reviewme/review/service/mapper/ReviewGatherMapper.java b/backend/src/main/java/reviewme/review/service/mapper/ReviewGatherMapper.java new file mode 100644 index 000000000..a21a6da6f --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/mapper/ReviewGatherMapper.java @@ -0,0 +1,85 @@ +package reviewme.review.service.mapper; + +import jakarta.annotation.Nullable; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import reviewme.question.domain.OptionItem; +import reviewme.question.domain.Question; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.Answer; +import reviewme.review.domain.CheckboxAnswer; +import reviewme.review.domain.CheckboxAnswerSelectedOption; +import reviewme.review.domain.TextAnswer; +import reviewme.review.service.dto.response.gathered.ReviewsGatheredByQuestionResponse; +import reviewme.review.service.dto.response.gathered.ReviewsGatheredBySectionResponse; +import reviewme.review.service.dto.response.gathered.SimpleQuestionResponse; +import reviewme.review.service.dto.response.gathered.TextResponse; +import reviewme.review.service.dto.response.gathered.VoteResponse; +import reviewme.review.service.exception.GatheredAnswersTypeNonUniformException; + +@Component +@RequiredArgsConstructor +public class ReviewGatherMapper { + + private final QuestionRepository questionRepository; + + public ReviewsGatheredBySectionResponse mapToReviewsGatheredBySection(Map> questionAnswers) { + List reviews = questionAnswers.entrySet() + .stream() + .map(entry -> mapToReviewsGatheredByQuestion(entry.getKey(), entry.getValue())) + .toList(); + + return new ReviewsGatheredBySectionResponse(reviews); + } + + private ReviewsGatheredByQuestionResponse mapToReviewsGatheredByQuestion(Question question, List answers) { + return new ReviewsGatheredByQuestionResponse( + new SimpleQuestionResponse(question.getId(), question.getContent(), question.getQuestionType()), + mapToTextResponse(question, answers), + mapToVoteResponse(question, answers) + ); + } + + @Nullable + private List mapToTextResponse(Question question, List answers) { + if (question.isSelectable()) { + return null; + } + + List textAnswers = castAllOrThrow(answers, TextAnswer.class); + return textAnswers.stream() + .map(textAnswer -> new TextResponse(textAnswer.getId(), textAnswer.getContent(), List.of())) + .toList(); + } + + @Nullable + private List mapToVoteResponse(Question question, List answers) { + if (!question.isSelectable()) { + return null; + } + + List checkboxAnswers = castAllOrThrow(answers, CheckboxAnswer.class); + Map optionItemIdVoteCount = checkboxAnswers.stream() + .flatMap(checkboxAnswer -> checkboxAnswer.getSelectedOptionIds().stream()) + .collect(Collectors.groupingBy(CheckboxAnswerSelectedOption::getSelectedOptionId, + Collectors.counting())); + + List allOptionItem = questionRepository.findAllOptionItemsByIdOrderByPosition(question.getId()); + return allOptionItem.stream() + .map(optionItem -> new VoteResponse( + optionItem.getContent(), + optionItemIdVoteCount.getOrDefault(optionItem.getId(), 0L))) + .toList(); + } + + private List castAllOrThrow(List answers, Class clazz) { + try { + return answers.stream().map(clazz::cast).toList(); + } catch (Exception ex) { + throw new GatheredAnswersTypeNonUniformException(ex); + } + } +} diff --git a/backend/src/main/java/reviewme/reviewgroup/domain/ReviewGroup.java b/backend/src/main/java/reviewme/reviewgroup/domain/ReviewGroup.java index 9da094186..ee5205424 100644 --- a/backend/src/main/java/reviewme/reviewgroup/domain/ReviewGroup.java +++ b/backend/src/main/java/reviewme/reviewgroup/domain/ReviewGroup.java @@ -44,12 +44,17 @@ public class ReviewGroup { private long templateId = 1L; public ReviewGroup(String reviewee, String projectName, String reviewRequestCode, String groupAccessCode) { + this(reviewee, projectName, reviewRequestCode, groupAccessCode, 1L); + } + + public ReviewGroup(String reviewee, String projectName, String reviewRequestCode, String groupAccessCode, long templateId) { validateRevieweeLength(reviewee); validateProjectNameLength(projectName); this.reviewee = reviewee; this.projectName = projectName; this.reviewRequestCode = reviewRequestCode; this.groupAccessCode = new GroupAccessCode(groupAccessCode); + this.templateId = templateId; } private void validateRevieweeLength(String reviewee) { diff --git a/backend/src/main/java/reviewme/template/repository/SectionRepository.java b/backend/src/main/java/reviewme/template/repository/SectionRepository.java index bcb36c92f..ae0aff9f6 100644 --- a/backend/src/main/java/reviewme/template/repository/SectionRepository.java +++ b/backend/src/main/java/reviewme/template/repository/SectionRepository.java @@ -1,6 +1,7 @@ package reviewme.template.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.stereotype.Repository; @@ -17,4 +18,12 @@ public interface SectionRepository extends JpaRepository { ORDER BY s.position ASC """, nativeQuery = true) List
findAllByTemplateId(long templateId); + + @Query(""" + SELECT s FROM Section s + JOIN TemplateSection ts ON s.id = ts.sectionId + WHERE ts.sectionId = :sectionId + AND ts.templateId = :templateId + """) + Optional
findByIdAndTemplateId(long sectionId, long templateId); } diff --git a/backend/src/test/java/reviewme/api/ApiTest.java b/backend/src/test/java/reviewme/api/ApiTest.java index 2d919e645..463e5f08e 100644 --- a/backend/src/test/java/reviewme/api/ApiTest.java +++ b/backend/src/test/java/reviewme/api/ApiTest.java @@ -29,7 +29,7 @@ import reviewme.highlight.controller.HighlightController; import reviewme.highlight.service.HighlightService; import reviewme.review.controller.ReviewController; -import reviewme.review.service.GatheredReviewLookupService; +import reviewme.review.service.ReviewGatheredLookupService; import reviewme.review.service.ReviewDetailLookupService; import reviewme.review.service.ReviewListLookupService; import reviewme.review.service.ReviewRegisterService; @@ -79,7 +79,7 @@ public abstract class ApiTest { protected SectionService sectionService; @MockBean - protected GatheredReviewLookupService gatheredReviewLookupService; + protected ReviewGatheredLookupService reviewGatheredLookupService; @MockBean protected HighlightService highlightService; diff --git a/backend/src/test/java/reviewme/api/ReviewApiTest.java b/backend/src/test/java/reviewme/api/ReviewApiTest.java index e8ae73a08..06a39f63b 100644 --- a/backend/src/test/java/reviewme/api/ReviewApiTest.java +++ b/backend/src/test/java/reviewme/api/ReviewApiTest.java @@ -24,13 +24,15 @@ import org.springframework.restdocs.request.ParameterDescriptor; import reviewme.question.domain.QuestionType; import reviewme.review.service.dto.request.ReviewRegisterRequest; -import reviewme.review.service.dto.response.gathered.AnswerContentResponse; +import reviewme.review.service.dto.response.gathered.HighlightResponse; +import reviewme.review.service.dto.response.gathered.RangeResponse; import reviewme.review.service.dto.response.gathered.ReviewsGatheredByQuestionResponse; import reviewme.review.service.dto.response.gathered.ReviewsGatheredBySectionResponse; import reviewme.review.service.dto.response.gathered.SimpleQuestionResponse; +import reviewme.review.service.dto.response.gathered.TextResponse; import reviewme.review.service.dto.response.gathered.VoteResponse; -import reviewme.review.service.dto.response.list.ReceivedReviewsSummaryResponse; import reviewme.review.service.dto.response.list.ReceivedReviewsResponse; +import reviewme.review.service.dto.response.list.ReceivedReviewsSummaryResponse; import reviewme.review.service.dto.response.list.ReviewCategoryResponse; import reviewme.review.service.dto.response.list.ReviewListElementResponse; import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; @@ -254,19 +256,22 @@ class ReviewApiTest extends ApiTest { void 자신이_받은_리뷰의_요약를_섹션별로_조회한다() { ReviewsGatheredBySectionResponse response = new ReviewsGatheredBySectionResponse(List.of( new ReviewsGatheredByQuestionResponse( - new SimpleQuestionResponse("서술형 질문", QuestionType.TEXT), + new SimpleQuestionResponse(1L, "서술형 질문", QuestionType.TEXT), List.of( - new AnswerContentResponse("산초의 답변"), - new AnswerContentResponse("삼촌의 답변")), + new TextResponse(1L, "산초의 답변", List.of( + new HighlightResponse(1, List.of(new RangeResponse(1, 10))), + new HighlightResponse(2, List.of(new RangeResponse(1, 4))) + )), + new TextResponse(2L, "삼촌의 답변", List.of())), null), new ReviewsGatheredByQuestionResponse( - new SimpleQuestionResponse("선택형 질문", QuestionType.CHECKBOX), + new SimpleQuestionResponse(2L, "선택형 질문", QuestionType.CHECKBOX), null, List.of( new VoteResponse("짜장", 3), new VoteResponse("짬뽕", 5)))) ); - BDDMockito.given(gatheredReviewLookupService.getReceivedReviewsBySectionId(anyString(), anyLong())) + BDDMockito.given(reviewGatheredLookupService.getReceivedReviewsBySectionId(anyString(), anyLong())) .willReturn(response); CookieDescriptor[] cookieDescriptors = { @@ -278,11 +283,20 @@ class ReviewApiTest extends ApiTest { FieldDescriptor[] responseFieldDescriptors = { fieldWithPath("reviews").description("리뷰 목록"), fieldWithPath("reviews[].question").description("질문 정보"), + fieldWithPath("reviews[].question.id").description("질문 ID"), fieldWithPath("reviews[].question.name").description("질문 이름"), fieldWithPath("reviews[].question.type").description("질문 유형"), fieldWithPath("reviews[].answers").description("서술형 답변 목록 - question.type이 TEXT가 아니면 null").optional(), + fieldWithPath("reviews[].answers[].id").description("답변 ID").optional(), fieldWithPath("reviews[].answers[].content").description("서술형 답변 내용"), - fieldWithPath("reviews[].votes").description("객관식 답변 목록 - question.type이 CHECKBOX가 아니면 null").optional(), + fieldWithPath("reviews[].answers[].highlights").description("형광펜 정보"), + fieldWithPath("reviews[].answers[].highlights[].lineIndex").description("개행으로 구분되는 라인 번호, 0-based"), + fieldWithPath("reviews[].answers[].highlights[].ranges").description("형광펜 범위"), + fieldWithPath("reviews[].answers[].highlights[].ranges[].startIndex").description( + "하이라이트 시작 인덱스, 0-based"), + fieldWithPath("reviews[].answers[].highlights[].ranges[].endIndex").description("하이라이트 끝 인덱스, 0-based"), + fieldWithPath("reviews[].votes").description( + "객관식 답변 목록 - question.type이 CHECKBOX가 아니면 null").optional(), fieldWithPath("reviews[].votes[].content").description("객관식 항목"), fieldWithPath("reviews[].votes[].count").description("선택한 사람 수"), }; diff --git a/backend/src/test/java/reviewme/question/repository/QuestionRepositoryTest.java b/backend/src/test/java/reviewme/question/repository/QuestionRepositoryTest.java index e0d427558..da694e335 100644 --- a/backend/src/test/java/reviewme/question/repository/QuestionRepositoryTest.java +++ b/backend/src/test/java/reviewme/question/repository/QuestionRepositoryTest.java @@ -1,7 +1,10 @@ package reviewme.question.repository; import static org.assertj.core.api.Assertions.assertThat; +import static reviewme.fixture.OptionGroupFixture.선택지_그룹; +import static reviewme.fixture.OptionItemFixture.선택지; import static reviewme.fixture.QuestionFixture.서술형_필수_질문; +import static reviewme.fixture.QuestionFixture.선택형_필수_질문; import static reviewme.fixture.SectionFixture.항상_보이는_섹션; import static reviewme.fixture.TemplateFixture.템플릿; @@ -10,7 +13,11 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import reviewme.question.domain.OptionGroup; +import reviewme.question.domain.OptionItem; import reviewme.question.domain.Question; +import reviewme.reviewgroup.domain.ReviewGroup; +import reviewme.reviewgroup.repository.ReviewGroupRepository; import reviewme.template.domain.Section; import reviewme.template.domain.Template; import reviewme.template.repository.SectionRepository; @@ -28,6 +35,15 @@ class QuestionRepositoryTest { @Autowired private TemplateRepository templateRepository; + @Autowired + private ReviewGroupRepository reviewGroupRepository; + + @Autowired + private OptionGroupRepository optionGroupRepository; + + @Autowired + private OptionItemRepository optionItemRepository; + @Test void 템플릿_아이디로_질문_목록_아이디를_모두_가져온다() { // given @@ -71,4 +87,49 @@ class QuestionRepositoryTest { // then assertThat(actual).containsExactlyInAnyOrder(question1, question2); } + + @Test + void 섹션_아이디에_해당하는_질문을_순서대로_가져온다() { + // given + Question question1 = questionRepository.save(서술형_필수_질문(1)); + Question question2 = questionRepository.save(서술형_필수_질문(2)); + Question question3 = questionRepository.save(서술형_필수_질문(3)); + Question question4 = questionRepository.save(서술형_필수_질문(1)); + + List sectionQuestion1 = List.of(question1.getId(), question2.getId(), question3.getId()); + List sectionQuestion2 = List.of(question4.getId()); + Section section1 = sectionRepository.save(항상_보이는_섹션(sectionQuestion1)); + Section section2 = sectionRepository.save(항상_보이는_섹션(sectionQuestion2)); + Template template = templateRepository.save(템플릿(List.of(section1.getId(), section2.getId()))); + + ReviewGroup reviewGroup = reviewGroupRepository.save(new ReviewGroup( + "reviewee", "projectName", "reviewRequestCode", "groupAccessCode", template.getId() + )); + + // when + List questionsInSection = questionRepository.findAllBySectionIdOrderByPosition(section1.getId()); + + // then + assertThat(questionsInSection).containsExactly(question1, question2, question3); + } + + @Test + void 질문_아이디에_해당하는_모든_옵션_아이템을_순서대로_불러온다() { + // given + Question question1 = questionRepository.save(선택형_필수_질문()); + Question question2 = questionRepository.save(선택형_필수_질문()); + OptionGroup optionGroup1 = optionGroupRepository.save(선택지_그룹(question1.getId())); + OptionGroup optionGroup2 = optionGroupRepository.save(선택지_그룹(question2.getId())); + + OptionItem optionItem1 = optionItemRepository.save(선택지(optionGroup1.getId())); + OptionItem optionItem2 = optionItemRepository.save(선택지(optionGroup1.getId())); + OptionItem optionItem3 = optionItemRepository.save(선택지(optionGroup2.getId())); + + // when + List optionItemsForQuestion1 + = questionRepository.findAllOptionItemsByIdOrderByPosition(question1.getId()); + + // then + assertThat(optionItemsForQuestion1).containsExactly(optionItem1, optionItem2); + } } diff --git a/backend/src/test/java/reviewme/review/repository/AnswerRepositoryTest.java b/backend/src/test/java/reviewme/review/repository/AnswerRepositoryTest.java index bd76f054d..e13ce1427 100644 --- a/backend/src/test/java/reviewme/review/repository/AnswerRepositoryTest.java +++ b/backend/src/test/java/reviewme/review/repository/AnswerRepositoryTest.java @@ -1,39 +1,80 @@ package reviewme.review.repository; import static org.assertj.core.api.Assertions.assertThat; +import static reviewme.fixture.QuestionFixture.서술형_필수_질문; +import static reviewme.fixture.ReviewGroupFixture.리뷰_그룹; +import static reviewme.fixture.SectionFixture.항상_보이는_섹션; +import static reviewme.fixture.TemplateFixture.템플릿; import java.util.List; import java.util.Set; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import reviewme.fixture.QuestionFixture; -import reviewme.fixture.ReviewGroupFixture; +import reviewme.question.domain.Question; import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.Answer; import reviewme.review.domain.Review; import reviewme.review.domain.TextAnswer; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; +import reviewme.template.domain.Section; +import reviewme.template.domain.Template; +import reviewme.template.repository.SectionRepository; +import reviewme.template.repository.TemplateRepository; @DataJpaTest class AnswerRepositoryTest { @Autowired - AnswerRepository answerRepository; + private AnswerRepository answerRepository; @Autowired - ReviewGroupRepository reviewGroupRepository; + private QuestionRepository questionRepository; @Autowired - ReviewRepository reviewRepository; + private SectionRepository sectionRepository; @Autowired - QuestionRepository questionRepository; + private TemplateRepository templateRepository; + + @Autowired + private ReviewGroupRepository reviewGroupRepository; + + @Autowired + private ReviewRepository reviewRepository; + + @Test + void 내가_받은_답변들_중_주어진_질문들에_대한_답변들을_최신_작성순으로_제한된_수만_반환한다() { + // given + Question question1 = questionRepository.save(서술형_필수_질문()); + Question question2 = questionRepository.save(서술형_필수_질문()); + Question question3 = questionRepository.save(서술형_필수_질문()); + Section section = sectionRepository.save(항상_보이는_섹션( + List.of(question1.getId(), question2.getId(), question3.getId()))); + Template template = templateRepository.save(템플릿(List.of(section.getId()))); + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + TextAnswer answer1 = new TextAnswer(question1.getId(), "답1".repeat(20)); + TextAnswer answer2 = new TextAnswer(question2.getId(), "답2".repeat(20)); + TextAnswer answer3 = new TextAnswer(question2.getId(), "답3".repeat(20)); + TextAnswer answer4 = new TextAnswer(question3.getId(), "답4".repeat(20)); + reviewRepository.save(new Review(template.getId(), reviewGroup.getId(), List.of(answer1))); + reviewRepository.save(new Review(template.getId(), reviewGroup.getId(), List.of(answer2))); + reviewRepository.save(new Review(template.getId(), reviewGroup.getId(), List.of(answer3))); + + // when + List actual = answerRepository.findReceivedAnswersByQuestionIds( + reviewGroup.getId(), List.of(question1.getId(), question2.getId()), 2); + + // then + assertThat(actual).containsExactly(answer3, answer2); + } @Test void 리뷰_그룹_id로_리뷰들을_찾아_id를_반환한다() { // given - ReviewGroup reviewGroup = reviewGroupRepository.save(ReviewGroupFixture.리뷰_그룹()); + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); TextAnswer answer1 = new TextAnswer(1L, "text answer1"); TextAnswer answer2 = new TextAnswer(1L, "text answer2"); Review review = reviewRepository.save(new Review(1L, reviewGroup.getId(), List.of(answer1, answer2))); @@ -48,9 +89,9 @@ class AnswerRepositoryTest { @Test void 질문_id로_리뷰들을_찾아_id를_반환한다() { // given - ReviewGroup reviewGroup = reviewGroupRepository.save(ReviewGroupFixture.리뷰_그룹()); - long questionId1 = questionRepository.save(QuestionFixture.서술형_필수_질문()).getId(); - long questionId2 = questionRepository.save(QuestionFixture.서술형_필수_질문()).getId(); + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + long questionId1 = questionRepository.save(서술형_필수_질문()).getId(); + long questionId2 = questionRepository.save(서술형_필수_질문()).getId(); TextAnswer textAnswer1_Q1 = new TextAnswer(questionId1, "text answer1 by Q1"); TextAnswer textAnswer2_Q1 = new TextAnswer(questionId1, "text answer2 by Q1"); TextAnswer textAnswer1_Q2 = new TextAnswer(questionId2, "text answer1 by Q2"); diff --git a/backend/src/test/java/reviewme/review/service/ReviewGatheredLookupServiceTest.java b/backend/src/test/java/reviewme/review/service/ReviewGatheredLookupServiceTest.java new file mode 100644 index 000000000..7a378adb5 --- /dev/null +++ b/backend/src/test/java/reviewme/review/service/ReviewGatheredLookupServiceTest.java @@ -0,0 +1,395 @@ +package reviewme.review.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static reviewme.fixture.OptionGroupFixture.선택지_그룹; +import static reviewme.fixture.OptionItemFixture.선택지; +import static reviewme.fixture.QuestionFixture.서술형_옵션_질문; +import static reviewme.fixture.QuestionFixture.서술형_필수_질문; +import static reviewme.fixture.QuestionFixture.선택형_옵션_질문; +import static reviewme.fixture.QuestionFixture.선택형_필수_질문; +import static reviewme.fixture.ReviewGroupFixture.리뷰_그룹; +import static reviewme.fixture.SectionFixture.항상_보이는_섹션; +import static reviewme.fixture.TemplateFixture.템플릿; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import reviewme.question.domain.OptionGroup; +import reviewme.question.domain.OptionItem; +import reviewme.question.domain.OptionType; +import reviewme.question.domain.Question; +import reviewme.question.repository.OptionGroupRepository; +import reviewme.question.repository.OptionItemRepository; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.CheckboxAnswer; +import reviewme.review.domain.Review; +import reviewme.review.domain.TextAnswer; +import reviewme.review.repository.ReviewRepository; +import reviewme.review.service.dto.response.gathered.ReviewsGatheredByQuestionResponse; +import reviewme.review.service.dto.response.gathered.ReviewsGatheredBySectionResponse; +import reviewme.review.service.dto.response.gathered.SimpleQuestionResponse; +import reviewme.review.service.dto.response.gathered.TextResponse; +import reviewme.review.service.dto.response.gathered.VoteResponse; +import reviewme.reviewgroup.domain.ReviewGroup; +import reviewme.reviewgroup.repository.ReviewGroupRepository; +import reviewme.support.ServiceTest; +import reviewme.template.domain.Section; +import reviewme.template.domain.Template; +import reviewme.template.repository.SectionRepository; +import reviewme.template.repository.TemplateRepository; + +@ServiceTest +class ReviewGatheredLookupServiceTest { + + @Autowired + private ReviewGroupRepository reviewGroupRepository; + + @Autowired + private ReviewRepository reviewRepository; + + @Autowired + private SectionRepository sectionRepository; + + @Autowired + private QuestionRepository questionRepository; + + @Autowired + private OptionGroupRepository optionGroupRepository; + + @Autowired + private OptionItemRepository optionItemRepository; + + @Autowired + private TemplateRepository templateRepository; + + @Autowired + private ReviewGatheredLookupService reviewLookupService; + + private String reviewRequestCode; + private ReviewGroup reviewGroup; + + @BeforeEach + void saveReviewGroup() { + reviewRequestCode = "1111"; + reviewGroup = reviewGroupRepository.save(리뷰_그룹(reviewRequestCode, "2222")); + } + + @Nested + @DisplayName("섹션에 해당하는 서술형 응답을 질문별로 묶어 반환한다") + class GatherAnswerByQuestionTest { + + @Test + void 섹션_하위_질문이_하나인_경우() { + // given - 질문 저장 + Question question1 = questionRepository.save(서술형_필수_질문()); + + // given - 섹션, 템플릿 저장 + Section section1 = sectionRepository.save(항상_보이는_섹션(List.of(question1.getId()))); + Template template = templateRepository.save(템플릿(List.of(section1.getId()))); + + // given - 리뷰 답변 저장 + TextAnswer answerKB = new TextAnswer(question1.getId(), "커비가 작성한 서술형 답변1"); + TextAnswer answerSC = new TextAnswer(question1.getId(), "산초가 작성한 서술형 답변1"); + reviewRepository.save(new Review(template.getId(), reviewGroup.getId(), List.of(answerKB))); + reviewRepository.save(new Review(template.getId(), reviewGroup.getId(), List.of(answerSC))); + + // when + ReviewsGatheredBySectionResponse actual = reviewLookupService.getReceivedReviewsBySectionId( + reviewRequestCode, section1.getId()); + + // then + assertThat(actual.reviews().get(0).answers()).extracting(TextResponse::content) + .containsOnly("커비가 작성한 서술형 답변1", "산초가 작성한 서술형 답변1"); + } + + @Test + void 섹션_하위_질문이_여러개인_경우() { + // given - 질문 저장 + Question question1 = questionRepository.save(서술형_필수_질문()); + Question question2 = questionRepository.save(서술형_필수_질문()); + + // given - 섹션, 템플릿 저장 + Section section1 = sectionRepository.save(항상_보이는_섹션(List.of(question1.getId(), question2.getId()))); + Template template = templateRepository.save(템플릿(List.of(section1.getId()))); + + // given - 리뷰 답변 저장 + TextAnswer answerAR1 = new TextAnswer(question1.getId(), "아루가 작성한 서술형 답변1"); + TextAnswer answerAR2 = new TextAnswer(question2.getId(), "아루가 작성한 서술형 답변2"); + TextAnswer answerTD1 = new TextAnswer(question1.getId(), "테드가 작성한 서술형 답변1"); + TextAnswer answerTD2 = new TextAnswer(question2.getId(), "테드가 작성한 서술형 답변2"); + reviewRepository.save(new Review(template.getId(), reviewGroup.getId(), List.of(answerAR1, answerAR2))); + reviewRepository.save(new Review(template.getId(), reviewGroup.getId(), List.of(answerTD1, answerTD2))); + + // when + ReviewsGatheredBySectionResponse actual = reviewLookupService.getReceivedReviewsBySectionId( + reviewRequestCode, section1.getId()); + + // then + assertThat(actual.reviews().get(0).answers()) + .extracting(TextResponse::content) + .containsExactlyInAnyOrder("아루가 작성한 서술형 답변1", "테드가 작성한 서술형 답변1"); + assertThat(actual.reviews().get(1).answers()) + .extracting(TextResponse::content) + .containsExactlyInAnyOrder("아루가 작성한 서술형 답변2", "테드가 작성한 서술형 답변2"); + } + + @Test + void 여러개의_섹션이_있는_경우_주어진_섹션ID에_해당하는_것만_반환한다() { + // given - 질문 저장 + Question question1 = questionRepository.save(서술형_필수_질문()); + Question question2 = questionRepository.save(서술형_필수_질문()); + + // given - 섹션, 템플릿 저장 + Section section1 = sectionRepository.save(항상_보이는_섹션(List.of(question1.getId()))); + Section section2 = sectionRepository.save(항상_보이는_섹션(List.of(question2.getId()))); + Template template = templateRepository.save(템플릿(List.of(section1.getId(), section2.getId()))); + + // given - 리뷰 답변 저장 + TextAnswer answerAR1 = new TextAnswer(question1.getId(), "아루가 작성한 서술형 답변1"); + TextAnswer answerAR2 = new TextAnswer(question2.getId(), "아루가 작성한 서술형 답변2"); + TextAnswer answerTD1 = new TextAnswer(question1.getId(), "테드가 작성한 서술형 답변1"); + TextAnswer answerTD2 = new TextAnswer(question2.getId(), "테드가 작성한 서술형 답변2"); + reviewRepository.save(new Review(template.getId(), reviewGroup.getId(), List.of(answerAR1, answerAR2))); + reviewRepository.save(new Review(template.getId(), reviewGroup.getId(), List.of(answerTD1, answerTD2))); + + // when + ReviewsGatheredBySectionResponse actual = reviewLookupService.getReceivedReviewsBySectionId( + reviewRequestCode, section1.getId()); + + // then + assertThat(actual.reviews().get(0).answers()) + .extracting(TextResponse::content) + .containsExactlyInAnyOrder("아루가 작성한 서술형 답변1", "테드가 작성한 서술형 답변1"); + } + + @Test + void 섹션에_필수가_아닌_질문이_있는_경우_답변된_내용만_반환한다() { + // given - 질문 저장 + Question question1 = questionRepository.save(서술형_옵션_질문()); + Question question2 = questionRepository.save(서술형_옵션_질문()); + + // given - 섹션, 템플릿 저장 + Section section1 = sectionRepository.save(항상_보이는_섹션(List.of(question1.getId(), question2.getId()))); + Template template = templateRepository.save(템플릿(List.of(section1.getId()))); + + // given - 리뷰 답변 저장 + TextAnswer answerSC1 = new TextAnswer(question1.getId(), "산초가 작성한 서술형 답변1"); + TextAnswer answerSC2 = new TextAnswer(question2.getId(), "산초가 작성한 서술형 답변2"); + TextAnswer answerAR = new TextAnswer(question1.getId(), "아루가 작성한 서술형 답변"); + reviewRepository.save(new Review(template.getId(), reviewGroup.getId(), List.of(answerSC1, answerSC2))); + reviewRepository.save(new Review(template.getId(), reviewGroup.getId(), List.of(answerAR))); + + // when + ReviewsGatheredBySectionResponse actual = reviewLookupService.getReceivedReviewsBySectionId( + reviewRequestCode, section1.getId()); + + // then + assertThat(actual.reviews().get(0).answers()) + .extracting(TextResponse::content) + .containsExactlyInAnyOrder("산초가 작성한 서술형 답변1", "아루가 작성한 서술형 답변"); + assertThat(actual.reviews().get(1).answers()) + .extracting(TextResponse::content) + .containsExactly("산초가 작성한 서술형 답변2"); + } + + @Test + void 질문에_응답이_없는_경우_질문_내용은_반환하되_응답은_빈_배열로_반환한다() { + // given - 질문 저장 + Question question1 = questionRepository.save(서술형_필수_질문()); + + // given - 섹션, 템플릿 저장 + Section section1 = sectionRepository.save(항상_보이는_섹션(List.of(question1.getId()))); + Template template = templateRepository.save(템플릿(List.of(section1.getId()))); + + // when + ReviewsGatheredBySectionResponse actual = reviewLookupService.getReceivedReviewsBySectionId( + reviewRequestCode, section1.getId()); + + // then + assertThat(actual.reviews()).hasSize(1); + assertThat(actual.reviews().get(0).question().name()).isEqualTo(question1.getContent()); + assertThat(actual.reviews().get(0).answers()).isEmpty(); + assertThat(actual.reviews().get(0).votes()).isNull(); + } + } + + @Nested + @DisplayName("섹션에 해당하는 선택형 응답을 질문별로 묶고, 선택된 횟수를 계산하여 반환한다") + class GatherOptionAnswerByQuestionTest { + + @Test + void 섹션_하위_질문이_하나인_경우() { + // given - 질문 저장 + Question question1 = questionRepository.save(선택형_필수_질문()); + OptionGroup optionGroup = optionGroupRepository.save(선택지_그룹(question1.getId())); + OptionItem optionItem1 = optionItemRepository.save( + new OptionItem("짜장", optionGroup.getId(), 1, OptionType.CATEGORY)); + OptionItem optionItem2 = optionItemRepository.save( + new OptionItem("짬뽕", optionGroup.getId(), 2, OptionType.CATEGORY)); + + // given - 섹션, 템플릿 저장 + Section section1 = sectionRepository.save(항상_보이는_섹션(List.of(question1.getId()))); + Template template = templateRepository.save(템플릿(List.of(section1.getId()))); + + // given - 리뷰 답변 저장 + CheckboxAnswer answer1 = new CheckboxAnswer(question1.getId(), List.of(optionItem1.getId())); + CheckboxAnswer answer2 = new CheckboxAnswer(question1.getId(), + List.of(optionItem1.getId(), optionItem2.getId())); + reviewRepository.save(new Review(template.getId(), reviewGroup.getId(), List.of(answer1))); + reviewRepository.save(new Review(template.getId(), reviewGroup.getId(), List.of(answer2))); + + // when + ReviewsGatheredBySectionResponse actual = reviewLookupService.getReceivedReviewsBySectionId( + reviewRequestCode, section1.getId()); + + // then + assertThat(actual.reviews().get(0).votes()) + .extracting(VoteResponse::content, VoteResponse::count) + .containsExactlyInAnyOrder( + tuple("짜장", 2L), + tuple("짬뽕", 1L) + ); + } + + @Test + void 섹션_하위_질문이_여러개인_경우() { + // given - 질문 저장 + Question question1 = questionRepository.save(선택형_옵션_질문()); + Question question2 = questionRepository.save(선택형_옵션_질문()); + OptionGroup optionGroup1 = optionGroupRepository.save(선택지_그룹(question1.getId())); + OptionGroup optionGroup2 = optionGroupRepository.save(선택지_그룹(question2.getId())); + OptionItem optionItem1 = optionItemRepository.save( + new OptionItem("중식", optionGroup1.getId(), 1, OptionType.CATEGORY)); + OptionItem optionItem2 = optionItemRepository.save( + new OptionItem("분식", optionGroup2.getId(), 2, OptionType.CATEGORY)); + + // given - 섹션, 템플릿 저장 + Section section1 = sectionRepository.save(항상_보이는_섹션(List.of(question1.getId(), question2.getId()))); + Template template = templateRepository.save(템플릿(List.of(section1.getId()))); + + // given - 리뷰 답변 저장 + CheckboxAnswer answer1 = new CheckboxAnswer(question1.getId(), List.of(optionItem1.getId())); + CheckboxAnswer answer2 = new CheckboxAnswer(question2.getId(), List.of(optionItem2.getId())); + reviewRepository.save(new Review(template.getId(), reviewGroup.getId(), List.of(answer1, answer2))); + + // when + ReviewsGatheredBySectionResponse actual = reviewLookupService.getReceivedReviewsBySectionId( + reviewRequestCode, section1.getId()); + + // then + assertThat(actual.reviews().get(0).votes()) + .extracting(VoteResponse::content, VoteResponse::count) + .containsOnly(tuple("중식", 1L)); + assertThat(actual.reviews().get(1).votes()) + .extracting(VoteResponse::content, VoteResponse::count) + .containsOnly(tuple("분식", 1L)); + } + + @Test + void 아무도_고르지_않은_선택지는_0개로_계산하여_반환한다() { + // given - 질문 저장 + Question question1 = questionRepository.save(선택형_필수_질문()); + OptionGroup optionGroup = optionGroupRepository.save(선택지_그룹(question1.getId())); + OptionItem optionItem1 = optionItemRepository.save( + new OptionItem("우테코 산초", optionGroup.getId(), 1, OptionType.CATEGORY)); + OptionItem optionItem2 = optionItemRepository.save( + new OptionItem("제이든 산초", optionGroup.getId(), 2, OptionType.CATEGORY)); + + // given - 섹션, 템플릿 저장 + Section section1 = sectionRepository.save(항상_보이는_섹션(List.of(question1.getId()))); + Template template = templateRepository.save(템플릿(List.of(section1.getId()))); + + // given - 리뷰 답변 저장 + CheckboxAnswer answer1 = new CheckboxAnswer(question1.getId(), List.of(optionItem1.getId())); + CheckboxAnswer answer2 = new CheckboxAnswer(question1.getId(), List.of(optionItem1.getId())); + reviewRepository.save(new Review(template.getId(), reviewGroup.getId(), List.of(answer1))); + reviewRepository.save(new Review(template.getId(), reviewGroup.getId(), List.of(answer2))); + + // when + ReviewsGatheredBySectionResponse actual = reviewLookupService.getReceivedReviewsBySectionId( + reviewRequestCode, section1.getId()); + + // then + assertThat(actual.reviews().get(0).votes()) + .extracting(VoteResponse::content, VoteResponse::count) + .containsExactlyInAnyOrder( + tuple("우테코 산초", 2L), + tuple("제이든 산초", 0L) + ); + } + } + + @Test + void 서술형_질문에_대한_응답과_선택형_질문에_대한_응답을_함께_반환한다() { + // given - 질문 저장 + Question question1 = questionRepository.save(서술형_필수_질문()); + Question question2 = questionRepository.save(선택형_필수_질문()); + OptionGroup optionGroup = optionGroupRepository.save(선택지_그룹(question2.getId())); + OptionItem optionItem1 = optionItemRepository.save(선택지(optionGroup.getId())); + OptionItem optionItem2 = optionItemRepository.save(선택지(optionGroup.getId())); + + // given - 섹션, 템플릿 저장 + Section section1 = sectionRepository.save(항상_보이는_섹션(List.of(question1.getId(), question2.getId()))); + Template template = templateRepository.save(템플릿(List.of(section1.getId()))); + + // given - 리뷰 답변 저장 + TextAnswer answer1 = new TextAnswer(question1.getId(), "아루가 작성한 서술형 답변"); + CheckboxAnswer answer2 = new CheckboxAnswer(question2.getId(), + List.of(optionItem1.getId(), optionItem2.getId())); + reviewRepository.save(new Review(template.getId(), reviewGroup.getId(), List.of(answer1, answer2))); + + // when + ReviewsGatheredBySectionResponse actual = reviewLookupService.getReceivedReviewsBySectionId( + reviewRequestCode, section1.getId()); + + // then + assertThat(actual.reviews()).hasSize(2); + assertThat(actual.reviews()) + .extracting(ReviewsGatheredByQuestionResponse::question) + .extracting(SimpleQuestionResponse::name) + .containsOnly(question1.getContent(), question2.getContent()); + assertThat(actual.reviews().get(0).answers()) + .extracting(TextResponse::content) + .containsExactly("아루가 작성한 서술형 답변"); + assertThat(actual.reviews().get(0).votes()).isNull(); + assertThat(actual.reviews().get(1).votes()) + .extracting(VoteResponse::content, VoteResponse::count) + .containsExactlyInAnyOrder( + tuple(optionItem1.getContent(), 1L), + tuple(optionItem2.getContent(), 1L) + ); + assertThat(actual.reviews().get(1).answers()).isNull(); + } + + @Test + void 다른_사람이_받은_리뷰는_포함하지_않는다() { + // given - 질문 저장 + Question question1 = questionRepository.save(서술형_필수_질문()); + Section section1 = sectionRepository.save(항상_보이는_섹션(List.of(question1.getId()))); + Template template = templateRepository.save(템플릿(List.of(section1.getId()))); + + String reviewRequestCodeBE = "review_me_be"; + ReviewGroup reviewGroupBE = new ReviewGroup("reviewee", "projectName", + reviewRequestCodeBE, "groupAccessCode", template.getId()); + ReviewGroup reviewGroupFE = new ReviewGroup("reviewee", "projectName", + "reviewRequestCode", "groupAccessCode", template.getId()); + reviewGroupRepository.saveAll(List.of(reviewGroupFE, reviewGroupBE)); + + // given - 리뷰 답변 저장 + TextAnswer answerFE = new TextAnswer(question1.getId(), "프론트엔드가 작성한 서술형 답변"); + TextAnswer answerBE = new TextAnswer(question1.getId(), "백엔드가 작성한 서술형 답변"); + reviewRepository.save(new Review(template.getId(), reviewGroupFE.getId(), List.of(answerFE))); + reviewRepository.save(new Review(template.getId(), reviewGroupBE.getId(), List.of(answerBE))); + + // when + ReviewsGatheredBySectionResponse actual = reviewLookupService.getReceivedReviewsBySectionId( + reviewRequestCodeBE, section1.getId()); + + // then + assertThat(actual.reviews()).hasSize(1); + } +} diff --git a/backend/src/test/java/reviewme/review/service/mapper/ReviewGatherMapperTest.java b/backend/src/test/java/reviewme/review/service/mapper/ReviewGatherMapperTest.java new file mode 100644 index 000000000..fdac6e0c3 --- /dev/null +++ b/backend/src/test/java/reviewme/review/service/mapper/ReviewGatherMapperTest.java @@ -0,0 +1,137 @@ +package reviewme.review.service.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static reviewme.fixture.OptionGroupFixture.선택지_그룹; +import static reviewme.fixture.OptionItemFixture.선택지; +import static reviewme.fixture.QuestionFixture.서술형_옵션_질문; +import static reviewme.fixture.QuestionFixture.선택형_옵션_질문; + +import java.util.List; +import java.util.Map; +import org.assertj.core.groups.Tuple; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import reviewme.question.domain.OptionGroup; +import reviewme.question.domain.OptionItem; +import reviewme.question.domain.Question; +import reviewme.question.repository.OptionGroupRepository; +import reviewme.question.repository.OptionItemRepository; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.CheckboxAnswer; +import reviewme.review.domain.Review; +import reviewme.review.domain.TextAnswer; +import reviewme.review.repository.ReviewRepository; +import reviewme.review.service.dto.response.gathered.ReviewsGatheredByQuestionResponse; +import reviewme.review.service.dto.response.gathered.ReviewsGatheredBySectionResponse; +import reviewme.review.service.dto.response.gathered.SimpleQuestionResponse; +import reviewme.review.service.dto.response.gathered.TextResponse; +import reviewme.review.service.dto.response.gathered.VoteResponse; +import reviewme.support.ServiceTest; +import reviewme.template.repository.SectionRepository; + +@ServiceTest +class ReviewGatherMapperTest { + + @Autowired + private ReviewGatherMapper reviewGatherMapper; + + @Autowired + private SectionRepository sectionRepository; + + @Autowired + private QuestionRepository questionRepository; + + @Autowired + private OptionGroupRepository optionGroupRepository; + + @Autowired + private OptionItemRepository optionItemRepository; + + @Autowired + private ReviewRepository reviewRepository; + + @Test + void 질문과_하위_답변을_규칙에_맞게_반환한다() { + // given + Question question1 = questionRepository.save(서술형_옵션_질문(1)); + Question question2 = questionRepository.save(선택형_옵션_질문(2)); + OptionGroup optionGroup = optionGroupRepository.save(선택지_그룹(question2.getId())); + OptionItem optionItem1 = optionItemRepository.save(선택지(optionGroup.getId())); + OptionItem optionItem2 = optionItemRepository.save(선택지(optionGroup.getId())); + optionItemRepository.saveAll(List.of(optionItem1, optionItem2)); + + TextAnswer textAnswer1 = new TextAnswer(question1.getId(), "프엔 서술형 답변"); + TextAnswer textAnswer2 = new TextAnswer(question1.getId(), "백엔드 서술형 답변"); + CheckboxAnswer checkboxAnswer = new CheckboxAnswer( + question2.getId(), List.of(optionItem1.getId(), optionItem2.getId())); + reviewRepository.save(new Review(1L, 1L, List.of(textAnswer1, textAnswer2, checkboxAnswer))); + + // when + ReviewsGatheredBySectionResponse actual = reviewGatherMapper.mapToReviewsGatheredBySection(Map.of( + question1, List.of(textAnswer1, textAnswer2), + question2, List.of(checkboxAnswer))); + + // then + assertAll( + () -> 질문의_수만큼_반환한다(actual, 2), + () -> 질문의_내용을_반환한다(actual, question1.getContent(), question2.getContent()), + () -> 서술형_답변을_반환한다(actual, "프엔 서술형 답변", "백엔드 서술형 답변"), + () -> 선택형_답변을_반환한다(actual, + Tuple.tuple(optionItem1.getContent(), 1L), + Tuple.tuple(optionItem2.getContent(), 1L)) + ); + } + + @Test + void 서술형_질문에_답변이_없으면_질문_정보는_반환하되_답변은_빈_배열로_반환한다() { + // given + Question question1 = questionRepository.save(서술형_옵션_질문(1)); + Question question2 = questionRepository.save(서술형_옵션_질문(2)); + + // when + ReviewsGatheredBySectionResponse actual = reviewGatherMapper.mapToReviewsGatheredBySection(Map.of( + question1, List.of(), + question2, List.of())); + + // then + assertAll( + () -> 질문의_수만큼_반환한다(actual, 2), + () -> 질문의_내용을_반환한다(actual, question1.getContent(), question2.getContent()), + () -> assertThat(actual.reviews()) + .flatExtracting(ReviewsGatheredByQuestionResponse::answers) + .isEmpty() + ); + } + + private void 질문의_수만큼_반환한다(ReviewsGatheredBySectionResponse actual, int expectedSize) { + assertThat(actual.reviews()).hasSize(expectedSize); + } + + private void 질문의_내용을_반환한다(ReviewsGatheredBySectionResponse actual, String... expectedContents) { + assertThat(actual.reviews()) + .extracting(ReviewsGatheredByQuestionResponse::question) + .extracting(SimpleQuestionResponse::name) + .containsExactly(expectedContents); + } + + private void 서술형_답변을_반환한다(ReviewsGatheredBySectionResponse actual, String... expectedAnswerContents) { + List textResponse = actual.reviews() + .stream() + .filter(review -> review.answers() != null) + .flatMap(reviewsGatheredByQuestionResponse -> reviewsGatheredByQuestionResponse.answers().stream()) + .toList(); + assertThat(textResponse).extracting(TextResponse::content).containsExactly(expectedAnswerContents); + } + + private void 선택형_답변을_반환한다(ReviewsGatheredBySectionResponse actual, Tuple... expectedVotes) { + List voteResponses = actual.reviews() + .stream() + .filter(review -> review.votes() != null) + .flatMap(reviewsGatheredByQuestionResponse -> reviewsGatheredByQuestionResponse.votes().stream()) + .toList(); + assertThat(voteResponses) + .extracting(VoteResponse::content, VoteResponse::count) + .containsExactly(expectedVotes); + } +} diff --git a/backend/src/test/java/reviewme/template/repository/SectionRepositoryTest.java b/backend/src/test/java/reviewme/template/repository/SectionRepositoryTest.java index 8bfa41dca..41e2699ed 100644 --- a/backend/src/test/java/reviewme/template/repository/SectionRepositoryTest.java +++ b/backend/src/test/java/reviewme/template/repository/SectionRepositoryTest.java @@ -1,10 +1,12 @@ package reviewme.template.repository; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; import static reviewme.fixture.SectionFixture.항상_보이는_섹션; import static reviewme.fixture.TemplateFixture.템플릿; import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; @@ -38,4 +40,23 @@ class SectionRepositoryTest { // then assertThat(actual).containsExactly(section1, section2, section3); } + + @Test + void 템플릿_아이디와_섹션_아이디에_해당하는_섹션을_반환한다() { + // given + List questionIds = List.of(1L); + Section section1 = sectionRepository.save(항상_보이는_섹션(questionIds)); + Section section2 = sectionRepository.save(항상_보이는_섹션(questionIds)); + Template template = templateRepository.save(템플릿(List.of(section1.getId()))); + + // when + Optional
actual1 = sectionRepository.findByIdAndTemplateId(section1.getId(), template.getId()); + Optional
actual2 = sectionRepository.findByIdAndTemplateId(section2.getId(), template.getId()); + + // then + assertAll( + () -> assertThat(actual1).isPresent(), + () -> assertThat(actual2).isEmpty() + ); + } }