diff --git a/backend/src/main/java/reviewme/review/controller/ReviewController.java b/backend/src/main/java/reviewme/review/controller/ReviewController.java index 4871f7377..64f08f9a3 100644 --- a/backend/src/main/java/reviewme/review/controller/ReviewController.java +++ b/backend/src/main/java/reviewme/review/controller/ReviewController.java @@ -11,11 +11,11 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import reviewme.global.HeaderProperty; -import reviewme.review.service.CreateReviewService; +import reviewme.review.service.ReviewRegisterService; import reviewme.review.service.ReviewDetailLookupService; -import reviewme.review.service.ReviewService; -import reviewme.review.service.dto.request.CreateReviewRequest; -import reviewme.review.service.dto.response.detail.TemplateAnswerResponse; +import reviewme.review.service.ReviewListLookupService; +import reviewme.review.service.dto.request.ReviewRegisterRequest; +import reviewme.review.service.dto.response.detail.ReviewDetailResponse; import reviewme.review.service.dto.response.list.ReceivedReviewsResponse; @RestController @@ -24,13 +24,13 @@ public class ReviewController { private static final String GROUP_ACCESS_CODE_HEADER = "GroupAccessCode"; - private final CreateReviewService createReviewService; - private final ReviewService reviewService; + private final ReviewRegisterService reviewRegisterService; + private final ReviewListLookupService reviewListLookupService; private final ReviewDetailLookupService reviewDetailLookupService; @PostMapping("/v2/reviews") - public ResponseEntity createReview(@Valid @RequestBody CreateReviewRequest request) { - long savedReviewId = createReviewService.createReview(request); + public ResponseEntity createReview(@Valid @RequestBody ReviewRegisterRequest request) { + long savedReviewId = reviewRegisterService.registerReview(request); return ResponseEntity.created(URI.create("/reviews/" + savedReviewId)).build(); } @@ -39,17 +39,17 @@ public ResponseEntity findReceivedReviews( @RequestParam String reviewRequestCode, @HeaderProperty(GROUP_ACCESS_CODE_HEADER) String groupAccessCode ) { - ReceivedReviewsResponse response = reviewService.findReceivedReviews(reviewRequestCode, groupAccessCode); + ReceivedReviewsResponse response = reviewListLookupService.getReceivedReviews(reviewRequestCode, groupAccessCode); return ResponseEntity.ok(response); } @GetMapping("/v2/reviews/{id}") - public ResponseEntity findReceivedReviewDetail( + public ResponseEntity findReceivedReviewDetail( @PathVariable long id, @RequestParam String reviewRequestCode, @HeaderProperty(GROUP_ACCESS_CODE_HEADER) String groupAccessCode ) { - TemplateAnswerResponse response = reviewDetailLookupService.getReviewDetail( + ReviewDetailResponse response = reviewDetailLookupService.getReviewDetail( id, reviewRequestCode, groupAccessCode ); return ResponseEntity.ok(response); diff --git a/backend/src/main/java/reviewme/review/domain/CheckboxAnswer.java b/backend/src/main/java/reviewme/review/domain/CheckboxAnswer.java index 6f6cf5aab..408b24076 100644 --- a/backend/src/main/java/reviewme/review/domain/CheckboxAnswer.java +++ b/backend/src/main/java/reviewme/review/domain/CheckboxAnswer.java @@ -15,6 +15,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; +import reviewme.review.domain.exception.QuestionNotAnsweredException; @Entity @Table(name = "checkbox_answer") @@ -38,9 +39,16 @@ public class CheckboxAnswer { private List selectedOptionIds; public CheckboxAnswer(long questionId, List selectedOptionIds) { + validateSelectedOptionIds(questionId, selectedOptionIds); this.questionId = questionId; this.selectedOptionIds = selectedOptionIds.stream() .map(CheckBoxAnswerSelectedOption::new) .toList(); } + + private void validateSelectedOptionIds(long questionId, List selectedOptionIds) { + if (selectedOptionIds == null || selectedOptionIds.isEmpty()) { + throw new QuestionNotAnsweredException(questionId); + } + } } diff --git a/backend/src/main/java/reviewme/review/domain/Review.java b/backend/src/main/java/reviewme/review/domain/Review.java index 4bf4a6856..899a41391 100644 --- a/backend/src/main/java/reviewme/review/domain/Review.java +++ b/backend/src/main/java/reviewme/review/domain/Review.java @@ -13,6 +13,9 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -55,6 +58,24 @@ public Review(long templateId, long reviewGroupId, this.createdAt = LocalDateTime.now(); } + public Set getAnsweredQuestionIds() { + return Stream.concat( + textAnswers.stream().map(TextAnswer::getQuestionId), + checkboxAnswers.stream().map(CheckboxAnswer::getQuestionId) + ).collect(Collectors.toSet()); + } + + public Set getAllCheckBoxOptionIds() { + return checkboxAnswers.stream() + .flatMap(answer -> answer.getSelectedOptionIds().stream()) + .map(CheckBoxAnswerSelectedOption::getSelectedOptionId) + .collect(Collectors.toSet()); + } + + public boolean hasAnsweredQuestion(long questionId) { + return getAnsweredQuestionIds().contains(questionId); + } + public LocalDate getCreatedDate() { return createdAt.toLocalDate(); } diff --git a/backend/src/main/java/reviewme/review/domain/TextAnswer.java b/backend/src/main/java/reviewme/review/domain/TextAnswer.java index ac54530a9..a7ba8e5ff 100644 --- a/backend/src/main/java/reviewme/review/domain/TextAnswer.java +++ b/backend/src/main/java/reviewme/review/domain/TextAnswer.java @@ -10,6 +10,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; +import reviewme.review.domain.exception.QuestionNotAnsweredException; @Entity @Table(name = "text_answer") @@ -29,7 +30,14 @@ public class TextAnswer { private String content; public TextAnswer(long questionId, String content) { + validateContent(questionId, content); this.questionId = questionId; this.content = content; } + + private void validateContent(long questionId, String content) { + if (content == null || content.isEmpty()) { + throw new QuestionNotAnsweredException(questionId); + } + } } diff --git a/backend/src/main/java/reviewme/review/domain/exception/QuestionNotAnsweredException.java b/backend/src/main/java/reviewme/review/domain/exception/QuestionNotAnsweredException.java new file mode 100644 index 000000000..270e75524 --- /dev/null +++ b/backend/src/main/java/reviewme/review/domain/exception/QuestionNotAnsweredException.java @@ -0,0 +1,13 @@ +package reviewme.review.domain.exception; + +import lombok.extern.slf4j.Slf4j; +import reviewme.global.exception.BadRequestException; + +@Slf4j +public class QuestionNotAnsweredException extends BadRequestException { + + public QuestionNotAnsweredException(long questionId) { + super("질문에 대한 답변을 작성하지 않았어요."); + log.warn("question must be answered - questionId: {}", questionId, this); + } +} diff --git a/backend/src/main/java/reviewme/review/service/CreateCheckBoxAnswerRequestValidator.java b/backend/src/main/java/reviewme/review/service/CreateCheckBoxAnswerRequestValidator.java deleted file mode 100644 index d9a10a434..000000000 --- a/backend/src/main/java/reviewme/review/service/CreateCheckBoxAnswerRequestValidator.java +++ /dev/null @@ -1,77 +0,0 @@ -package reviewme.review.service; - -import java.util.HashSet; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -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.service.dto.request.CreateReviewAnswerRequest; -import reviewme.review.service.exception.CheckBoxAnswerIncludedNotProvidedOptionItemException; -import reviewme.review.service.exception.CheckBoxAnswerIncludedTextException; -import reviewme.review.service.exception.RequiredQuestionNotAnsweredException; -import reviewme.review.service.exception.SelectedOptionItemCountOutOfRangeException; -import reviewme.review.service.exception.SubmittedQuestionNotFoundException; -import reviewme.template.domain.exception.OptionGroupNotFoundByQuestionIdException; - -@Component -@RequiredArgsConstructor -public class CreateCheckBoxAnswerRequestValidator { - - private final QuestionRepository questionRepository; - private final OptionGroupRepository optionGroupRepository; - private final OptionItemRepository optionItemRepository; - - public void validate(CreateReviewAnswerRequest request) { - validateNotContainingText(request); - Question question = questionRepository.findById(request.questionId()) - .orElseThrow(() -> new SubmittedQuestionNotFoundException(request.questionId())); - OptionGroup optionGroup = optionGroupRepository.findByQuestionId(question.getId()) - .orElseThrow(() -> new OptionGroupNotFoundByQuestionIdException(question.getId())); - validateRequiredQuestion(request, question); - validateOnlyIncludingProvidedOptionItem(request, optionGroup); - validateCheckedOptionItemCount(request, optionGroup); - } - - private void validateNotContainingText(CreateReviewAnswerRequest request) { - if (request.text() != null) { - throw new CheckBoxAnswerIncludedTextException(); - } - } - - private void validateRequiredQuestion(CreateReviewAnswerRequest request, Question question) { - if (question.isRequired() && request.selectedOptionIds() == null) { - throw new RequiredQuestionNotAnsweredException(question.getId()); - } - } - - private void validateOnlyIncludingProvidedOptionItem(CreateReviewAnswerRequest request, OptionGroup optionGroup) { - List providedOptionItemIds = optionItemRepository.findAllByOptionGroupId(optionGroup.getId()) - .stream() - .map(OptionItem::getId) - .toList(); - List submittedOptionItemIds = request.selectedOptionIds(); - - if (!new HashSet<>(providedOptionItemIds).containsAll(submittedOptionItemIds)) { - throw new CheckBoxAnswerIncludedNotProvidedOptionItemException( - request.questionId(), providedOptionItemIds, submittedOptionItemIds - ); - } - } - - private void validateCheckedOptionItemCount(CreateReviewAnswerRequest request, OptionGroup optionGroup) { - if (request.selectedOptionIds().size() < optionGroup.getMinSelectionCount() - || request.selectedOptionIds().size() > optionGroup.getMaxSelectionCount()) { - throw new SelectedOptionItemCountOutOfRangeException( - request.questionId(), - request.selectedOptionIds().size(), - optionGroup.getMinSelectionCount(), - optionGroup.getMaxSelectionCount() - ); - } - } -} diff --git a/backend/src/main/java/reviewme/review/service/CreateReviewService.java b/backend/src/main/java/reviewme/review/service/CreateReviewService.java deleted file mode 100644 index d6225f133..000000000 --- a/backend/src/main/java/reviewme/review/service/CreateReviewService.java +++ /dev/null @@ -1,144 +0,0 @@ -package reviewme.review.service; - -import java.util.ArrayList; -import java.util.HashSet; -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 reviewme.question.domain.Question; -import reviewme.question.domain.QuestionType; -import reviewme.question.repository.QuestionRepository; -import reviewme.review.domain.CheckboxAnswer; -import reviewme.review.domain.Review; -import reviewme.review.domain.TextAnswer; -import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException; -import reviewme.review.repository.ReviewRepository; -import reviewme.review.service.dto.request.CreateReviewAnswerRequest; -import reviewme.review.service.dto.request.CreateReviewRequest; -import reviewme.review.service.exception.MissingRequiredQuestionException; -import reviewme.review.service.exception.SubmittedQuestionAndProvidedQuestionMismatchException; -import reviewme.review.service.exception.SubmittedQuestionNotFoundException; -import reviewme.review.service.exception.UnnecessaryQuestionIncludedException; -import reviewme.reviewgroup.domain.ReviewGroup; -import reviewme.reviewgroup.repository.ReviewGroupRepository; -import reviewme.template.domain.SectionQuestion; -import reviewme.template.domain.Template; -import reviewme.template.domain.exception.TemplateNotFoundByReviewGroupException; -import reviewme.template.repository.SectionRepository; -import reviewme.template.repository.TemplateRepository; - -@Service -@RequiredArgsConstructor -public class CreateReviewService { - - private final ReviewRepository reviewRepository; - private final QuestionRepository questionRepository; - private final ReviewGroupRepository reviewGroupRepository; - private final CreateTextAnswerRequestValidator createTextAnswerRequestValidator; - private final CreateCheckBoxAnswerRequestValidator createCheckBoxAnswerRequestValidator; - private final TemplateRepository templateRepository; - private final SectionRepository sectionRepository; - - @Transactional - public long createReview(CreateReviewRequest request) { - ReviewGroup reviewGroup = validateReviewGroupByRequestCode(request.reviewRequestCode()); - Template template = templateRepository.findById(reviewGroup.getTemplateId()) - .orElseThrow(() -> new TemplateNotFoundByReviewGroupException( - reviewGroup.getId(), reviewGroup.getTemplateId())); - validateSubmittedQuestionsContainedInTemplate(reviewGroup.getTemplateId(), request); - validateOnlyRequiredQuestionsSubmitted(template, request); - - return saveReview(request, reviewGroup); - } - - private ReviewGroup validateReviewGroupByRequestCode(String reviewRequestCode) { - return reviewGroupRepository.findByReviewRequestCode(reviewRequestCode) - .orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode)); - } - - private void validateSubmittedQuestionsContainedInTemplate(long templateId, CreateReviewRequest request) { - Set providedQuestionIds = questionRepository.findAllQuestionIdByTemplateId(templateId); - Set submittedQuestionIds = request.answers() - .stream() - .map(CreateReviewAnswerRequest::questionId) - .collect(Collectors.toSet()); - if (!providedQuestionIds.containsAll(submittedQuestionIds)) { - throw new SubmittedQuestionAndProvidedQuestionMismatchException(submittedQuestionIds, providedQuestionIds); - } - } - - private void validateOnlyRequiredQuestionsSubmitted(Template template, CreateReviewRequest request) { - // 제출된 리뷰의 옵션 아이템 ID 목록 - List selectedOptionItemIds = request.answers() - .stream() - .filter(answer -> answer.selectedOptionIds() != null) - .flatMap(answer -> answer.selectedOptionIds().stream()) - .toList(); - - // 제출된 리뷰의 질문 ID 목록 - List submittedQuestionIds = request.answers() - .stream() - .map(CreateReviewAnswerRequest::questionId) - .toList(); - - // 섹션에서 답해야 할 질문 ID 목록 - List requiredQuestionIdsCandidates = sectionRepository.findAllByTemplateId(template.getId()) - .stream() - // 선택된 optionItem 에 따라 required 를 다르게 책정해서 필터링 - .filter(section -> section.isVisibleBySelectedOptionIds(selectedOptionItemIds)) - .flatMap(section -> section.getQuestionIds().stream()) - .map(SectionQuestion::getQuestionId) - .toList(); - List requiredQuestionIds = questionRepository.findAllById(requiredQuestionIdsCandidates) - .stream() - .filter(Question::isRequired) - .map(Question::getId) - .toList(); - - // 제출된 리뷰의 질문 중에서 제출해야 할 질문이 모두 포함되었는지 검사 - Set submittedQuestionIds2 = new HashSet<>(submittedQuestionIds); - if (!submittedQuestionIds2.containsAll(requiredQuestionIds)) { - List missingRequiredQuestionIds = new ArrayList<>(requiredQuestionIds); - missingRequiredQuestionIds.removeAll(submittedQuestionIds2); - throw new MissingRequiredQuestionException(missingRequiredQuestionIds); - } - - // 제출된 리뷰의 질문 중에서 필수가 아닌 질문이 포함되었는지 검사 - requiredQuestionIds.forEach(submittedQuestionIds2::remove); - List unnecessaryQuestionIds = questionRepository.findAllById(submittedQuestionIds2) - .stream() - .filter(Question::isRequired) - .map(Question::getId) - .toList(); - if (!unnecessaryQuestionIds.isEmpty()) { - throw new UnnecessaryQuestionIncludedException(unnecessaryQuestionIds); - } - } - - private Long saveReview(CreateReviewRequest request, ReviewGroup reviewGroup) { - List textAnswers = new ArrayList<>(); - List checkboxAnswers = new ArrayList<>(); - for (CreateReviewAnswerRequest answerRequests : request.answers()) { - Question question = questionRepository.findById(answerRequests.questionId()) - .orElseThrow(() -> new SubmittedQuestionNotFoundException(answerRequests.questionId())); - QuestionType questionType = question.getQuestionType(); - if (questionType == QuestionType.TEXT && answerRequests.isNotBlank()) { - createTextAnswerRequestValidator.validate(answerRequests); - textAnswers.add(new TextAnswer(question.getId(), answerRequests.text())); - continue; - } - if (questionType == QuestionType.CHECKBOX) { - createCheckBoxAnswerRequestValidator.validate(answerRequests); - checkboxAnswers.add(new CheckboxAnswer(question.getId(), answerRequests.selectedOptionIds())); - } - } - - Review savedReview = reviewRepository.save( - new Review(reviewGroup.getTemplateId(), reviewGroup.getId(), textAnswers, checkboxAnswers) - ); - return savedReview.getId(); - } -} diff --git a/backend/src/main/java/reviewme/review/service/CreateTextAnswerRequestValidator.java b/backend/src/main/java/reviewme/review/service/CreateTextAnswerRequestValidator.java deleted file mode 100644 index f47d03852..000000000 --- a/backend/src/main/java/reviewme/review/service/CreateTextAnswerRequestValidator.java +++ /dev/null @@ -1,48 +0,0 @@ -package reviewme.review.service; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import reviewme.question.domain.Question; -import reviewme.question.repository.QuestionRepository; -import reviewme.review.domain.exception.InvalidTextAnswerLengthException; -import reviewme.review.service.dto.request.CreateReviewAnswerRequest; -import reviewme.review.service.exception.RequiredQuestionNotAnsweredException; -import reviewme.review.service.exception.SubmittedQuestionNotFoundException; -import reviewme.review.service.exception.TextAnswerIncludedOptionItemException; - -@Component -@RequiredArgsConstructor -public class CreateTextAnswerRequestValidator { - - private static final int MIN_LENGTH = 20; - private static final int MAX_LENGTH = 1_000; - - private final QuestionRepository questionRepository; - - public void validate(CreateReviewAnswerRequest request) { - Question question = questionRepository.findById(request.questionId()) - .orElseThrow(() -> new SubmittedQuestionNotFoundException(request.questionId())); - validateNotIncludingOptions(request); - validateQuestionRequired(question, request); - validateLength(request); - } - - private void validateNotIncludingOptions(CreateReviewAnswerRequest request) { - if (request.selectedOptionIds() != null) { - throw new TextAnswerIncludedOptionItemException(); - } - } - - private void validateQuestionRequired(Question question, CreateReviewAnswerRequest request) { - if (question.isRequired() && request.text() == null) { - throw new RequiredQuestionNotAnsweredException(question.getId()); - } - } - - private void validateLength(CreateReviewAnswerRequest request) { - int textLength = request.text().length(); - if (textLength < MIN_LENGTH || textLength > MAX_LENGTH) { - throw new InvalidTextAnswerLengthException(textLength, MIN_LENGTH, MAX_LENGTH); - } - } -} diff --git a/backend/src/main/java/reviewme/review/service/ReviewDetailLookupService.java b/backend/src/main/java/reviewme/review/service/ReviewDetailLookupService.java index eb9fb04f9..e551918c2 100644 --- a/backend/src/main/java/reviewme/review/service/ReviewDetailLookupService.java +++ b/backend/src/main/java/reviewme/review/service/ReviewDetailLookupService.java @@ -1,155 +1,43 @@ package reviewme.review.service; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import reviewme.question.domain.OptionGroup; -import reviewme.question.domain.Question; -import reviewme.question.repository.OptionGroupRepository; -import reviewme.question.repository.OptionItemRepository; -import reviewme.question.repository.QuestionRepository; -import reviewme.review.domain.CheckboxAnswers; import reviewme.review.domain.Review; -import reviewme.review.domain.TextAnswer; -import reviewme.review.domain.TextAnswers; import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException; import reviewme.review.repository.ReviewRepository; -import reviewme.review.service.dto.response.detail.OptionGroupAnswerResponse; -import reviewme.review.service.dto.response.detail.OptionItemAnswerResponse; -import reviewme.review.service.dto.response.detail.QuestionAnswerResponse; -import reviewme.review.service.dto.response.detail.SectionAnswerResponse; -import reviewme.review.service.dto.response.detail.TemplateAnswerResponse; +import reviewme.review.service.dto.response.detail.ReviewDetailResponse; import reviewme.review.service.exception.ReviewGroupUnauthorizedException; import reviewme.review.service.exception.ReviewNotFoundByIdAndGroupException; +import reviewme.review.service.module.ReviewDetailMapper; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; -import reviewme.template.domain.Section; -import reviewme.template.domain.exception.OptionGroupNotFoundByQuestionIdException; -import reviewme.template.repository.SectionRepository; @Service @Transactional(readOnly = true) @AllArgsConstructor public class ReviewDetailLookupService { - private final SectionRepository sectionRepository; private final ReviewRepository reviewRepository; private final ReviewGroupRepository reviewGroupRepository; - private final QuestionRepository questionRepository; - private final OptionItemRepository optionItemRepository; - private final OptionGroupRepository optionGroupRepository; - public TemplateAnswerResponse getReviewDetail(long reviewId, String reviewRequestCode, String groupAccessCode) { - ReviewGroup reviewGroup = reviewGroupRepository.findByReviewRequestCode(reviewRequestCode) - .orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode)); - if (!reviewGroup.matchesGroupAccessCode(groupAccessCode)) { - throw new ReviewGroupUnauthorizedException(reviewGroup.getId()); - } - Review review = reviewRepository.findByIdAndReviewGroupId(reviewId, reviewGroup.getId()) - .orElseThrow(() -> new ReviewNotFoundByIdAndGroupException(reviewId, reviewGroup.getId())); - - long templateId = review.getTemplateId(); - - List
sections = sectionRepository.findAllByTemplateId(templateId); - List sectionResponses = new ArrayList<>(); - - for (Section section : sections) { - addSectionResponse(review, reviewGroup, section, sectionResponses); - } - - return new TemplateAnswerResponse( - templateId, - reviewGroup.getReviewee(), - reviewGroup.getProjectName(), - review.getCreatedDate(), - sectionResponses - ); - } - - private void addSectionResponse(Review review, ReviewGroup reviewGroup, - Section section, List sectionResponses) { - ArrayList questionResponses = new ArrayList<>(); - - for (Question question : questionRepository.findAllBySectionId(section.getId())) { - if (question.isSelectable()) { - addCheckboxQuestionResponse(review, reviewGroup, question, questionResponses); - } else { - addTextQuestionResponse(review, reviewGroup, question, questionResponses); - } - } + private final ReviewDetailMapper reviewDetailMapper; - if (!questionResponses.isEmpty()) { - sectionResponses.add(new SectionAnswerResponse( - section.getId(), - section.convertHeader("{revieweeName}", reviewGroup.getReviewee()), - questionResponses - )); - } - } + public ReviewDetailResponse getReviewDetail(long reviewId, String reviewRequestCode, String groupAccessCode) { + ReviewGroup reviewGroup = reviewGroupRepository.findByReviewRequestCode(reviewRequestCode) + .orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode)); - private void addCheckboxQuestionResponse(Review review, ReviewGroup reviewGroup, - Question question, ArrayList questionResponses) { - CheckboxAnswers checkboxAnswers = new CheckboxAnswers(review.getCheckboxAnswers()); + validateGroupAccessCode(reviewGroup, groupAccessCode); - if (checkboxAnswers.hasAnswerByQuestionId(question.getId())) { - questionResponses.add(getCheckboxAnswerResponse(review, question, reviewGroup)); - } + Review review = reviewRepository.findByIdAndReviewGroupId(reviewId, reviewGroup.getId()) + .orElseThrow(() -> new ReviewNotFoundByIdAndGroupException(reviewId, reviewGroup.getId())); + return reviewDetailMapper.mapToReviewDetailResponse(review, reviewGroup); } - private void addTextQuestionResponse(Review review, ReviewGroup reviewGroup, - Question question, ArrayList questionResponses) { - TextAnswers textAnswers = new TextAnswers(review.getTextAnswers()); - - if (textAnswers.hasAnswerByQuestionId(question.getId())) { - questionResponses.add(getTextAnswerResponse(textAnswers, question, reviewGroup)); + private void validateGroupAccessCode(ReviewGroup reviewGroup, String groupAccessCode) { + if (!reviewGroup.matchesGroupAccessCode(groupAccessCode)) { + throw new ReviewGroupUnauthorizedException(reviewGroup.getId()); } } - - private QuestionAnswerResponse getCheckboxAnswerResponse(Review review, Question question, - ReviewGroup reviewGroup) { - OptionGroup optionGroup = optionGroupRepository.findByQuestionId(question.getId()) - .orElseThrow(() -> new OptionGroupNotFoundByQuestionIdException(question.getId())); - Set selectedOptionItemIds = optionItemRepository.findSelectedOptionItemIdsByReviewId(review.getId()); - List optionItemResponse = - optionItemRepository.findSelectedOptionItemsByReviewIdAndQuestionId(review.getId(), question.getId()) - .stream() - .map(optionItem -> new OptionItemAnswerResponse( - optionItem.getId(), - optionItem.getContent(), - selectedOptionItemIds.contains(optionItem.getId())) - ).toList(); - - OptionGroupAnswerResponse optionGroupAnswerResponse = new OptionGroupAnswerResponse( - optionGroup.getId(), - optionGroup.getMinSelectionCount(), - optionGroup.getMaxSelectionCount(), - optionItemResponse - ); - - return new QuestionAnswerResponse( - question.getId(), - question.isRequired(), - question.getQuestionType(), - question.convertContent("{revieweeName}", reviewGroup.getReviewee()), - optionGroupAnswerResponse, - null - ); - } - - private QuestionAnswerResponse getTextAnswerResponse(TextAnswers textAnswers, Question question, - ReviewGroup reviewGroup) { - TextAnswer textAnswer = textAnswers.getAnswerByQuestionId(question.getId()); - return new QuestionAnswerResponse( - question.getId(), - question.isRequired(), - question.getQuestionType(), - question.convertContent("{revieweeName}", reviewGroup.getReviewee()), - null, - textAnswer.getContent() - ); - } } diff --git a/backend/src/main/java/reviewme/review/service/ReviewListLookupService.java b/backend/src/main/java/reviewme/review/service/ReviewListLookupService.java new file mode 100644 index 000000000..e768dcd57 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/ReviewListLookupService.java @@ -0,0 +1,42 @@ +package reviewme.review.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.review.service.dto.response.list.ReceivedReviewsResponse; +import reviewme.review.service.dto.response.list.ReviewListElementResponse; +import reviewme.review.service.exception.ReviewGroupUnauthorizedException; +import reviewme.review.service.module.ReviewListMapper; +import reviewme.reviewgroup.domain.ReviewGroup; +import reviewme.reviewgroup.repository.ReviewGroupRepository; + +@Service +@RequiredArgsConstructor +public class ReviewListLookupService { + + private final ReviewGroupRepository reviewGroupRepository; + private final ReviewListMapper reviewListMapper; + + @Transactional(readOnly = true) + public ReceivedReviewsResponse getReceivedReviews(String reviewRequestCode, String groupAccessCode) { + ReviewGroup reviewGroup = findReviewGroupByRequestCodeOrThrow(reviewRequestCode); + validateGroupAccessCode(groupAccessCode, reviewGroup); + + List reviewGroupResponse = reviewListMapper.mapToReviewList(reviewGroup); + return new ReceivedReviewsResponse(reviewGroup.getReviewee(), reviewGroup.getProjectName(), + reviewGroupResponse); + } + + private ReviewGroup findReviewGroupByRequestCodeOrThrow(String reviewRequestCode) { + return reviewGroupRepository.findByReviewRequestCode(reviewRequestCode) + .orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode)); + } + + private static void validateGroupAccessCode(String groupAccessCode, ReviewGroup reviewGroup) { + if (!reviewGroup.matchesGroupAccessCode(groupAccessCode)) { + throw new ReviewGroupUnauthorizedException(reviewGroup.getId()); + } + } +} diff --git a/backend/src/main/java/reviewme/review/service/ReviewRegisterService.java b/backend/src/main/java/reviewme/review/service/ReviewRegisterService.java new file mode 100644 index 000000000..d30c5c2c0 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/ReviewRegisterService.java @@ -0,0 +1,28 @@ +package reviewme.review.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import reviewme.review.domain.Review; +import reviewme.review.repository.ReviewRepository; +import reviewme.review.service.dto.request.ReviewRegisterRequest; +import reviewme.review.service.module.ReviewMapper; +import reviewme.review.service.module.ReviewValidator; + +@Service +@RequiredArgsConstructor +public class ReviewRegisterService { + + private final ReviewMapper reviewMapper; + private final ReviewValidator reviewValidator; + + private final ReviewRepository reviewRepository; + + @Transactional + public long registerReview(ReviewRegisterRequest request) { + Review review = reviewMapper.mapToReview(request); + reviewValidator.validate(review); + Review registeredReview = reviewRepository.save(review); + return registeredReview.getId(); + } +} diff --git a/backend/src/main/java/reviewme/review/service/ReviewService.java b/backend/src/main/java/reviewme/review/service/ReviewService.java deleted file mode 100644 index 1ac3d0b3b..000000000 --- a/backend/src/main/java/reviewme/review/service/ReviewService.java +++ /dev/null @@ -1,63 +0,0 @@ -package reviewme.review.service; - -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import reviewme.question.domain.OptionItem; -import reviewme.question.domain.OptionType; -import reviewme.question.repository.OptionItemRepository; -import reviewme.review.domain.Review; -import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException; -import reviewme.review.repository.ReviewRepository; -import reviewme.review.service.dto.response.list.ReceivedReviewCategoryResponse; -import reviewme.review.service.dto.response.list.ReceivedReviewResponse; -import reviewme.review.service.dto.response.list.ReceivedReviewsResponse; -import reviewme.review.service.exception.ReviewGroupUnauthorizedException; -import reviewme.reviewgroup.domain.ReviewGroup; -import reviewme.reviewgroup.repository.ReviewGroupRepository; - -@Service -@RequiredArgsConstructor -public class ReviewService { - - private final ReviewGroupRepository reviewGroupRepository; - private final OptionItemRepository optionItemRepository; - private final ReviewRepository reviewRepository; - - private final ReviewPreviewGenerator reviewPreviewGenerator = new ReviewPreviewGenerator(); - - @Transactional(readOnly = true) - public ReceivedReviewsResponse findReceivedReviews(String reviewRequestCode, String groupAccessCode) { - ReviewGroup reviewGroup = reviewGroupRepository.findByReviewRequestCode(reviewRequestCode) - .orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode)); - - if (!reviewGroup.matchesGroupAccessCode(groupAccessCode)) { - throw new ReviewGroupUnauthorizedException(reviewGroup.getId()); - } - - List reviewResponses = - reviewRepository.findReceivedReviewsByGroupId(reviewGroup.getId()) - .stream() - .map(this::createReceivedReviewResponse) - .toList(); - - return new ReceivedReviewsResponse(reviewGroup.getReviewee(), reviewGroup.getProjectName(), reviewResponses); - } - - private ReceivedReviewResponse createReceivedReviewResponse(Review review) { - List categoryOptionItems = optionItemRepository.findByReviewIdAndOptionType(review.getId(), - OptionType.CATEGORY); - - List categoryResponses = categoryOptionItems.stream() - .map(optionItem -> new ReceivedReviewCategoryResponse(optionItem.getId(), optionItem.getContent())) - .toList(); - - return new ReceivedReviewResponse( - review.getId(), - review.getCreatedAt().toLocalDate(), - reviewPreviewGenerator.generatePreview(review.getTextAnswers()), - categoryResponses - ); - } -} diff --git a/backend/src/main/java/reviewme/review/service/dto/request/CreateReviewAnswerRequest.java b/backend/src/main/java/reviewme/review/service/dto/request/ReviewAnswerRequest.java similarity index 72% rename from backend/src/main/java/reviewme/review/service/dto/request/CreateReviewAnswerRequest.java rename to backend/src/main/java/reviewme/review/service/dto/request/ReviewAnswerRequest.java index 32ee6b238..2da5ab5ec 100644 --- a/backend/src/main/java/reviewme/review/service/dto/request/CreateReviewAnswerRequest.java +++ b/backend/src/main/java/reviewme/review/service/dto/request/ReviewAnswerRequest.java @@ -4,7 +4,7 @@ import jakarta.validation.constraints.NotNull; import java.util.List; -public record CreateReviewAnswerRequest( +public record ReviewAnswerRequest( @NotNull(message = "질문 ID를 입력해주세요.") Long questionId, @@ -15,7 +15,4 @@ public record CreateReviewAnswerRequest( @Nullable String text ) { - public boolean isNotBlank() { - return text != null && !text.isBlank(); - } } diff --git a/backend/src/main/java/reviewme/review/service/dto/request/CreateReviewRequest.java b/backend/src/main/java/reviewme/review/service/dto/request/ReviewRegisterRequest.java similarity index 80% rename from backend/src/main/java/reviewme/review/service/dto/request/CreateReviewRequest.java rename to backend/src/main/java/reviewme/review/service/dto/request/ReviewRegisterRequest.java index bfb47b769..1b7a6f896 100644 --- a/backend/src/main/java/reviewme/review/service/dto/request/CreateReviewRequest.java +++ b/backend/src/main/java/reviewme/review/service/dto/request/ReviewRegisterRequest.java @@ -4,12 +4,12 @@ import jakarta.validation.constraints.NotEmpty; import java.util.List; -public record CreateReviewRequest( +public record ReviewRegisterRequest( @NotBlank(message = "리뷰 요청 코드를 입력해주세요.") String reviewRequestCode, @NotEmpty(message = "답변 내용을 입력해주세요.") - List answers + List answers ) { } diff --git a/backend/src/main/java/reviewme/review/service/dto/response/detail/TemplateAnswerResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/detail/ReviewDetailResponse.java similarity index 87% rename from backend/src/main/java/reviewme/review/service/dto/response/detail/TemplateAnswerResponse.java rename to backend/src/main/java/reviewme/review/service/dto/response/detail/ReviewDetailResponse.java index 0e838236b..84e60cf97 100644 --- a/backend/src/main/java/reviewme/review/service/dto/response/detail/TemplateAnswerResponse.java +++ b/backend/src/main/java/reviewme/review/service/dto/response/detail/ReviewDetailResponse.java @@ -3,7 +3,7 @@ import java.time.LocalDate; import java.util.List; -public record TemplateAnswerResponse( +public record ReviewDetailResponse( long formId, String revieweeName, String projectName, diff --git a/backend/src/main/java/reviewme/review/service/dto/response/detail/SectionAnswerResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/detail/SectionAnswerResponse.java index ad2887644..eb45bddb2 100644 --- a/backend/src/main/java/reviewme/review/service/dto/response/detail/SectionAnswerResponse.java +++ b/backend/src/main/java/reviewme/review/service/dto/response/detail/SectionAnswerResponse.java @@ -7,4 +7,8 @@ public record SectionAnswerResponse( String header, List questions ) { + + public boolean hasAnsweredQuestion() { + return !questions.isEmpty(); + } } diff --git a/backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewsResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewsResponse.java index 877b3a0de..e15cf92c6 100644 --- a/backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewsResponse.java +++ b/backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewsResponse.java @@ -5,6 +5,6 @@ public record ReceivedReviewsResponse( String revieweeName, String projectName, - List reviews + List reviews ) { } diff --git a/backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewCategoryResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/list/ReviewCategoryResponse.java similarity index 69% rename from backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewCategoryResponse.java rename to backend/src/main/java/reviewme/review/service/dto/response/list/ReviewCategoryResponse.java index 298e78faa..cb9d0cc7f 100644 --- a/backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewCategoryResponse.java +++ b/backend/src/main/java/reviewme/review/service/dto/response/list/ReviewCategoryResponse.java @@ -1,6 +1,6 @@ package reviewme.review.service.dto.response.list; -public record ReceivedReviewCategoryResponse( +public record ReviewCategoryResponse( long optionId, String content ) { diff --git a/backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/list/ReviewListElementResponse.java similarity index 67% rename from backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewResponse.java rename to backend/src/main/java/reviewme/review/service/dto/response/list/ReviewListElementResponse.java index fa6804c18..07aa32c9f 100644 --- a/backend/src/main/java/reviewme/review/service/dto/response/list/ReceivedReviewResponse.java +++ b/backend/src/main/java/reviewme/review/service/dto/response/list/ReviewListElementResponse.java @@ -3,10 +3,10 @@ import java.time.LocalDate; import java.util.List; -public record ReceivedReviewResponse( +public record ReviewListElementResponse( long reviewId, LocalDate createdAt, String contentPreview, - List categories + List categories ) { } diff --git a/backend/src/main/java/reviewme/review/service/exception/RequiredQuestionNotAnsweredException.java b/backend/src/main/java/reviewme/review/service/exception/RequiredQuestionNotAnsweredException.java deleted file mode 100644 index 0367b93f6..000000000 --- a/backend/src/main/java/reviewme/review/service/exception/RequiredQuestionNotAnsweredException.java +++ /dev/null @@ -1,13 +0,0 @@ -package reviewme.review.service.exception; - -import lombok.extern.slf4j.Slf4j; -import reviewme.global.exception.BadRequestException; - -@Slf4j -public class RequiredQuestionNotAnsweredException extends BadRequestException { - - public RequiredQuestionNotAnsweredException(long questionId) { - super("필수 질문의 답변을 작성하지 않았어요."); - log.warn("Required question must be answered - questionId: {}", questionId, this); - } -} diff --git a/backend/src/main/java/reviewme/review/service/module/AnswerMapper.java b/backend/src/main/java/reviewme/review/service/module/AnswerMapper.java new file mode 100644 index 000000000..f43537119 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/module/AnswerMapper.java @@ -0,0 +1,28 @@ +package reviewme.review.service.module; + +import org.springframework.stereotype.Component; +import reviewme.review.domain.CheckboxAnswer; +import reviewme.review.domain.TextAnswer; +import reviewme.review.service.dto.request.ReviewAnswerRequest; +import reviewme.review.service.exception.CheckBoxAnswerIncludedTextException; +import reviewme.review.service.exception.TextAnswerIncludedOptionItemException; + +@Component +public class AnswerMapper { + + public TextAnswer mapToTextAnswer(ReviewAnswerRequest answerRequest) { + if (answerRequest.selectedOptionIds() != null) { + throw new TextAnswerIncludedOptionItemException(); + } + + return new TextAnswer(answerRequest.questionId(), answerRequest.text()); + } + + public CheckboxAnswer mapToCheckBoxAnswer(ReviewAnswerRequest answerRequest) { + if (answerRequest.text() != null) { + throw new CheckBoxAnswerIncludedTextException(); + } + + return new CheckboxAnswer(answerRequest.questionId(), answerRequest.selectedOptionIds()); + } +} diff --git a/backend/src/main/java/reviewme/review/service/module/CheckBoxAnswerValidator.java b/backend/src/main/java/reviewme/review/service/module/CheckBoxAnswerValidator.java new file mode 100644 index 000000000..3fc4c7a50 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/module/CheckBoxAnswerValidator.java @@ -0,0 +1,73 @@ +package reviewme.review.service.module; + +import java.util.HashSet; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +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.CheckBoxAnswerSelectedOption; +import reviewme.review.domain.CheckboxAnswer; +import reviewme.review.service.exception.CheckBoxAnswerIncludedNotProvidedOptionItemException; +import reviewme.review.service.exception.SelectedOptionItemCountOutOfRangeException; +import reviewme.review.service.exception.SubmittedQuestionNotFoundException; +import reviewme.template.domain.exception.OptionGroupNotFoundByQuestionIdException; + +@Component +@RequiredArgsConstructor +public class CheckBoxAnswerValidator { + + private final QuestionRepository questionRepository; + private final OptionGroupRepository optionGroupRepository; + private final OptionItemRepository optionItemRepository; + + public void validate(CheckboxAnswer checkboxAnswer) { + Question question = questionRepository.findById(checkboxAnswer.getQuestionId()) + .orElseThrow(() -> new SubmittedQuestionNotFoundException(checkboxAnswer.getQuestionId())); + + OptionGroup optionGroup = optionGroupRepository.findByQuestionId(question.getId()) + .orElseThrow(() -> new OptionGroupNotFoundByQuestionIdException(question.getId())); + + validateOnlyIncludingProvidedOptionItem(checkboxAnswer, optionGroup); + validateCheckedOptionItemCount(checkboxAnswer, optionGroup); + } + + private void validateOnlyIncludingProvidedOptionItem(CheckboxAnswer checkboxAnswer, OptionGroup optionGroup) { + List providedOptionItemIds = optionItemRepository.findAllByOptionGroupId(optionGroup.getId()) + .stream() + .map(OptionItem::getId) + .toList(); + List answeredOptionItemIds = extractAnsweredOptionItemIds(checkboxAnswer); + + if (!new HashSet<>(providedOptionItemIds).containsAll(answeredOptionItemIds)) { + throw new CheckBoxAnswerIncludedNotProvidedOptionItemException( + checkboxAnswer.getQuestionId(), providedOptionItemIds, answeredOptionItemIds + ); + } + } + + private void validateCheckedOptionItemCount(CheckboxAnswer checkboxAnswer, OptionGroup optionGroup) { + int answeredOptionItemCount = extractAnsweredOptionItemIds(checkboxAnswer).size(); + + if (answeredOptionItemCount < optionGroup.getMinSelectionCount() + || answeredOptionItemCount > optionGroup.getMaxSelectionCount()) { + throw new SelectedOptionItemCountOutOfRangeException( + checkboxAnswer.getQuestionId(), + answeredOptionItemCount, + optionGroup.getMinSelectionCount(), + optionGroup.getMaxSelectionCount() + ); + } + } + + private List extractAnsweredOptionItemIds(CheckboxAnswer checkboxAnswer) { + return checkboxAnswer.getSelectedOptionIds() + .stream() + .map(CheckBoxAnswerSelectedOption::getSelectedOptionId) + .toList(); + } +} diff --git a/backend/src/main/java/reviewme/review/service/module/ReviewDetailMapper.java b/backend/src/main/java/reviewme/review/service/module/ReviewDetailMapper.java new file mode 100644 index 000000000..fa544d59c --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/module/ReviewDetailMapper.java @@ -0,0 +1,120 @@ +package reviewme.review.service.module; + +import java.util.List; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import reviewme.question.domain.OptionGroup; +import reviewme.question.domain.Question; +import reviewme.question.repository.OptionGroupRepository; +import reviewme.question.repository.OptionItemRepository; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.Review; +import reviewme.review.domain.TextAnswer; +import reviewme.review.domain.TextAnswers; +import reviewme.review.service.dto.response.detail.OptionGroupAnswerResponse; +import reviewme.review.service.dto.response.detail.OptionItemAnswerResponse; +import reviewme.review.service.dto.response.detail.QuestionAnswerResponse; +import reviewme.review.service.dto.response.detail.ReviewDetailResponse; +import reviewme.review.service.dto.response.detail.SectionAnswerResponse; +import reviewme.reviewgroup.domain.ReviewGroup; +import reviewme.template.domain.Section; +import reviewme.template.domain.exception.OptionGroupNotFoundByQuestionIdException; +import reviewme.template.repository.SectionRepository; + +@Component +@RequiredArgsConstructor +public class ReviewDetailMapper { + + public static final String REVIEWEE_NAME_PLACEHOLDER = "{revieweeName}"; + + private final SectionRepository sectionRepository; + private final QuestionRepository questionRepository; + private final OptionGroupRepository optionGroupRepository; + private final OptionItemRepository optionItemRepository; + + public ReviewDetailResponse mapToReviewDetailResponse(Review review, ReviewGroup reviewGroup) { + long templateId = review.getTemplateId(); + List
sections = sectionRepository.findAllByTemplateId(templateId); + List sectionResponses = sections.stream() + .map(section -> mapToSectionResponse(review, reviewGroup, section)) + .filter(SectionAnswerResponse::hasAnsweredQuestion) + .toList(); + + return new ReviewDetailResponse( + templateId, + reviewGroup.getReviewee(), + reviewGroup.getProjectName(), + review.getCreatedDate(), + sectionResponses + ); + } + + private SectionAnswerResponse mapToSectionResponse(Review review, ReviewGroup reviewGroup, Section section) { + List questionResponses = questionRepository.findAllBySectionId(section.getId()) + .stream() + .filter(question -> review.hasAnsweredQuestion(question.getId())) + .map(question -> mapToQuestionResponse(review, reviewGroup, question)) + .toList(); + + return new SectionAnswerResponse( + section.getId(), + section.convertHeader(REVIEWEE_NAME_PLACEHOLDER, reviewGroup.getReviewee()), + questionResponses + ); + } + + private QuestionAnswerResponse mapToQuestionResponse(Review review, ReviewGroup reviewGroup, Question question) { + if (question.isSelectable()) { + return mapToCheckboxQuestionResponse(review, reviewGroup, question); + } else { + return mapToTextQuestionResponse(review, reviewGroup, question); + } + } + + private QuestionAnswerResponse mapToCheckboxQuestionResponse(Review review, ReviewGroup reviewGroup, + Question question) { + OptionGroup optionGroup = optionGroupRepository.findByQuestionId(question.getId()) + .orElseThrow(() -> new OptionGroupNotFoundByQuestionIdException(question.getId())); + Set selectedOptionItemIds = optionItemRepository.findSelectedOptionItemIdsByReviewId(review.getId()); + + List optionItemResponse = + optionItemRepository.findSelectedOptionItemsByReviewIdAndQuestionId(review.getId(), question.getId()) + .stream() + .map(optionItem -> new OptionItemAnswerResponse( + optionItem.getId(), + optionItem.getContent(), + selectedOptionItemIds.contains(optionItem.getId())) + ).toList(); + + OptionGroupAnswerResponse optionGroupAnswerResponse = new OptionGroupAnswerResponse( + optionGroup.getId(), + optionGroup.getMinSelectionCount(), + optionGroup.getMaxSelectionCount(), + optionItemResponse + ); + + return new QuestionAnswerResponse( + question.getId(), + question.isRequired(), + question.getQuestionType(), + question.convertContent(REVIEWEE_NAME_PLACEHOLDER, reviewGroup.getReviewee()), + optionGroupAnswerResponse, + null + ); + } + + private QuestionAnswerResponse mapToTextQuestionResponse(Review review, ReviewGroup reviewGroup, + Question question) { + TextAnswers textAnswers = new TextAnswers(review.getTextAnswers()); + TextAnswer textAnswer = textAnswers.getAnswerByQuestionId(question.getId()); + return new QuestionAnswerResponse( + question.getId(), + question.isRequired(), + question.getQuestionType(), + question.convertContent(REVIEWEE_NAME_PLACEHOLDER, reviewGroup.getReviewee()), + null, + textAnswer.getContent() + ); + } +} diff --git a/backend/src/main/java/reviewme/review/service/module/ReviewListMapper.java b/backend/src/main/java/reviewme/review/service/module/ReviewListMapper.java new file mode 100644 index 000000000..c7251c388 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/module/ReviewListMapper.java @@ -0,0 +1,46 @@ +package reviewme.review.service.module; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import reviewme.question.domain.OptionItem; +import reviewme.question.domain.OptionType; +import reviewme.question.repository.OptionItemRepository; +import reviewme.review.domain.Review; +import reviewme.review.repository.ReviewRepository; +import reviewme.review.service.dto.response.list.ReviewCategoryResponse; +import reviewme.review.service.dto.response.list.ReviewListElementResponse; +import reviewme.reviewgroup.domain.ReviewGroup; + +@Component +@RequiredArgsConstructor +public class ReviewListMapper { + + private final OptionItemRepository optionItemRepository; + private final ReviewRepository reviewRepository; + + private final ReviewPreviewGenerator reviewPreviewGenerator = new ReviewPreviewGenerator(); + + public List mapToReviewList(ReviewGroup reviewGroup) { + return reviewRepository.findReceivedReviewsByGroupId(reviewGroup.getId()) + .stream() + .map(this::mapToReviewListElementResponse) + .toList(); + } + + private ReviewListElementResponse mapToReviewListElementResponse(Review review) { + List categoryOptionItems = optionItemRepository + .findByReviewIdAndOptionType(review.getId(), OptionType.CATEGORY); + + List categoryResponses = categoryOptionItems.stream() + .map(optionItem -> new ReviewCategoryResponse(optionItem.getId(), optionItem.getContent())) + .toList(); + + return new ReviewListElementResponse( + review.getId(), + review.getCreatedDate(), + reviewPreviewGenerator.generatePreview(review.getTextAnswers()), + categoryResponses + ); + } +} diff --git a/backend/src/main/java/reviewme/review/service/module/ReviewMapper.java b/backend/src/main/java/reviewme/review/service/module/ReviewMapper.java new file mode 100644 index 000000000..90b94ee4d --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/module/ReviewMapper.java @@ -0,0 +1,84 @@ +package reviewme.review.service.module; + +import java.util.ArrayList; +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.Question; +import reviewme.question.domain.QuestionType; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.CheckboxAnswer; +import reviewme.review.domain.Review; +import reviewme.review.domain.TextAnswer; +import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.review.service.dto.request.ReviewAnswerRequest; +import reviewme.review.service.dto.request.ReviewRegisterRequest; +import reviewme.review.service.exception.SubmittedQuestionNotFoundException; +import reviewme.reviewgroup.domain.ReviewGroup; +import reviewme.reviewgroup.repository.ReviewGroupRepository; +import reviewme.template.domain.Template; +import reviewme.template.domain.exception.TemplateNotFoundByReviewGroupException; +import reviewme.template.repository.TemplateRepository; + +@Component +@RequiredArgsConstructor +public class ReviewMapper { + + private final AnswerMapper answerMapper; + private final ReviewGroupRepository reviewGroupRepository; + private final QuestionRepository questionRepository; + private final TemplateRepository templateRepository; + + public Review mapToReview(ReviewRegisterRequest request) { + ReviewGroup reviewGroup = findReviewGroupByRequestCodeOrThrow(request.reviewRequestCode()); + Template template = findTemplateByReviewGroupOrThrow(reviewGroup); + + List textAnswers = new ArrayList<>(); + List checkboxAnswers = new ArrayList<>(); + addAnswersByQuestionType(request, textAnswers, checkboxAnswers); + + return new Review(template.getId(), reviewGroup.getId(), textAnswers, checkboxAnswers); + } + + private ReviewGroup findReviewGroupByRequestCodeOrThrow(String reviewRequestCode) { + return reviewGroupRepository.findByReviewRequestCode(reviewRequestCode) + .orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode)); + } + + private Template findTemplateByReviewGroupOrThrow(ReviewGroup reviewGroup) { + return templateRepository.findById(reviewGroup.getTemplateId()) + .orElseThrow(() -> new TemplateNotFoundByReviewGroupException( + reviewGroup.getId(), reviewGroup.getTemplateId())); + } + + private void addAnswersByQuestionType(ReviewRegisterRequest request, + List textAnswers, List checkboxAnswers) { + List questionIds = request.answers() + .stream() + .map(ReviewAnswerRequest::questionId) + .toList(); + + Map questionMap = questionRepository.findAllById(questionIds) + .stream() + .collect(Collectors.toMap(Question::getId, question -> question)); + + for (ReviewAnswerRequest answerRequest : request.answers()) { + Question question = questionMap.get(answerRequest.questionId()); + if (question == null) { + throw new SubmittedQuestionNotFoundException(answerRequest.questionId()); + } + + if (question.getQuestionType() == QuestionType.TEXT) { + TextAnswer textAnswer = answerMapper.mapToTextAnswer(answerRequest); + textAnswers.add(textAnswer); + } + + if (question.getQuestionType() == QuestionType.CHECKBOX) { + CheckboxAnswer checkboxAnswer = answerMapper.mapToCheckBoxAnswer(answerRequest); + checkboxAnswers.add(checkboxAnswer); + } + } + } +} diff --git a/backend/src/main/java/reviewme/review/service/ReviewPreviewGenerator.java b/backend/src/main/java/reviewme/review/service/module/ReviewPreviewGenerator.java similarity index 93% rename from backend/src/main/java/reviewme/review/service/ReviewPreviewGenerator.java rename to backend/src/main/java/reviewme/review/service/module/ReviewPreviewGenerator.java index e0cbebc71..53c0ab9f7 100644 --- a/backend/src/main/java/reviewme/review/service/ReviewPreviewGenerator.java +++ b/backend/src/main/java/reviewme/review/service/module/ReviewPreviewGenerator.java @@ -1,4 +1,4 @@ -package reviewme.review.service; +package reviewme.review.service.module; import java.util.List; import reviewme.review.domain.TextAnswer; diff --git a/backend/src/main/java/reviewme/review/service/module/ReviewValidator.java b/backend/src/main/java/reviewme/review/service/module/ReviewValidator.java new file mode 100644 index 000000000..704c57b50 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/module/ReviewValidator.java @@ -0,0 +1,75 @@ +package reviewme.review.service.module; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import reviewme.question.domain.Question; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.CheckboxAnswer; +import reviewme.review.domain.Review; +import reviewme.review.domain.TextAnswer; +import reviewme.review.service.exception.MissingRequiredQuestionException; +import reviewme.review.service.exception.SubmittedQuestionAndProvidedQuestionMismatchException; +import reviewme.template.domain.Section; +import reviewme.template.domain.SectionQuestion; +import reviewme.template.repository.SectionRepository; + +@Component +@RequiredArgsConstructor +public class ReviewValidator { + + private final TextAnswerValidator textAnswerValidator; + private final CheckBoxAnswerValidator checkBoxAnswerValidator; + + private final SectionRepository sectionRepository; + private final QuestionRepository questionRepository; + + public void validate(Review review) { + validateAnswer(review.getTextAnswers(), review.getCheckboxAnswers()); + validateAllAnswersContainedInTemplate(review); + validateAllRequiredQuestionsAnswered(review); + } + + private void validateAnswer(List textAnswers, List checkboxAnswers) { + textAnswers.forEach(textAnswerValidator::validate); + checkboxAnswers.forEach(checkBoxAnswerValidator::validate); + } + + private void validateAllAnswersContainedInTemplate(Review review) { + Set providedQuestionIds = questionRepository.findAllQuestionIdByTemplateId(review.getTemplateId()); + Set reviewedQuestionIds = review.getAnsweredQuestionIds(); + if (!providedQuestionIds.containsAll(reviewedQuestionIds)) { + throw new SubmittedQuestionAndProvidedQuestionMismatchException(reviewedQuestionIds, providedQuestionIds); + } + } + + private void validateAllRequiredQuestionsAnswered(Review review) { + Set displayedQuestionIds = extractDisplayedQuestionIds(review); + Set requiredQuestionIds = questionRepository.findAllById(displayedQuestionIds) + .stream() + .filter(Question::isRequired) + .map(Question::getId) + .collect(Collectors.toSet()); + + Set reviewedQuestionIds = review.getAnsweredQuestionIds(); + if (!reviewedQuestionIds.containsAll(requiredQuestionIds)) { + List missingRequiredQuestionIds = new ArrayList<>(requiredQuestionIds); + missingRequiredQuestionIds.removeAll(reviewedQuestionIds); + throw new MissingRequiredQuestionException(missingRequiredQuestionIds); + } + } + + private Set extractDisplayedQuestionIds(Review review) { + Set selectedOptionIds = review.getAllCheckBoxOptionIds(); + List
sections = sectionRepository.findAllByTemplateId(review.getTemplateId()); + + return sections.stream() + .filter(section -> section.isVisibleBySelectedOptionIds(selectedOptionIds)) + .flatMap(section -> section.getQuestionIds().stream()) + .map(SectionQuestion::getQuestionId) + .collect(Collectors.toSet()); + } +} diff --git a/backend/src/main/java/reviewme/review/service/module/TextAnswerValidator.java b/backend/src/main/java/reviewme/review/service/module/TextAnswerValidator.java new file mode 100644 index 000000000..5e22c00ff --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/module/TextAnswerValidator.java @@ -0,0 +1,36 @@ +package reviewme.review.service.module; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.TextAnswer; +import reviewme.review.domain.exception.InvalidTextAnswerLengthException; +import reviewme.review.service.exception.SubmittedQuestionNotFoundException; + +@Component +@RequiredArgsConstructor +public class TextAnswerValidator { + + private static final int MIN_LENGTH = 20; + private static final int MAX_LENGTH = 1_000; + + private final QuestionRepository questionRepository; + + public void validate(TextAnswer textAnswer) { + validateExistQuestion(textAnswer); + validateLength(textAnswer); + } + + private void validateExistQuestion(TextAnswer textAnswer) { + if (!questionRepository.existsById(textAnswer.getQuestionId())) { + throw new SubmittedQuestionNotFoundException(textAnswer.getQuestionId()); + } + } + + private void validateLength(TextAnswer textAnswer) { + int answerLength = textAnswer.getContent().length(); + if (answerLength < MIN_LENGTH || answerLength > MAX_LENGTH) { + throw new InvalidTextAnswerLengthException(answerLength, MIN_LENGTH, MAX_LENGTH); + } + } +} diff --git a/backend/src/main/java/reviewme/template/service/TemplateService.java b/backend/src/main/java/reviewme/template/service/TemplateService.java index 905de7ce9..fdf7a9326 100644 --- a/backend/src/main/java/reviewme/template/service/TemplateService.java +++ b/backend/src/main/java/reviewme/template/service/TemplateService.java @@ -6,29 +6,24 @@ import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; -import reviewme.template.domain.Template; -import reviewme.template.domain.exception.TemplateNotFoundByReviewGroupException; -import reviewme.template.repository.TemplateRepository; import reviewme.template.service.dto.response.TemplateResponse; +import reviewme.template.service.module.TemplateMapper; @Service @RequiredArgsConstructor public class TemplateService { private final ReviewGroupRepository reviewGroupRepository; - private final TemplateRepository templateRepository; private final TemplateMapper templateMapper; @Transactional(readOnly = true) public TemplateResponse generateReviewForm(String reviewRequestCode) { - ReviewGroup reviewGroup = reviewGroupRepository.findByReviewRequestCode(reviewRequestCode) - .orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode)); - - Template template = templateRepository.findById(reviewGroup.getTemplateId()) - .orElseThrow(() -> new TemplateNotFoundByReviewGroupException( - reviewGroup.getId(), reviewGroup.getTemplateId() - )); + ReviewGroup reviewGroup = findReviewGroupByRequestCodeOrThrow(reviewRequestCode); + return templateMapper.mapToTemplateResponse(reviewGroup); + } - return templateMapper.mapToTemplateResponse(reviewGroup, template); + private ReviewGroup findReviewGroupByRequestCodeOrThrow(String reviewRequestCode) { + return reviewGroupRepository.findByReviewRequestCode(reviewRequestCode) + .orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode)); } } diff --git a/backend/src/main/java/reviewme/template/service/TemplateMapper.java b/backend/src/main/java/reviewme/template/service/module/TemplateMapper.java similarity index 84% rename from backend/src/main/java/reviewme/template/service/TemplateMapper.java rename to backend/src/main/java/reviewme/template/service/module/TemplateMapper.java index 15b5cb0a2..1fbfa6f03 100644 --- a/backend/src/main/java/reviewme/template/service/TemplateMapper.java +++ b/backend/src/main/java/reviewme/template/service/module/TemplateMapper.java @@ -1,4 +1,4 @@ -package reviewme.template.service; +package reviewme.template.service.module; import java.util.List; import lombok.RequiredArgsConstructor; @@ -16,7 +16,9 @@ import reviewme.template.domain.Template; import reviewme.template.domain.TemplateSection; import reviewme.template.domain.exception.SectionInTemplateNotFoundException; +import reviewme.template.domain.exception.TemplateNotFoundByReviewGroupException; import reviewme.template.repository.SectionRepository; +import reviewme.template.repository.TemplateRepository; import reviewme.template.service.dto.response.OptionGroupResponse; import reviewme.template.service.dto.response.OptionItemResponse; import reviewme.template.service.dto.response.QuestionResponse; @@ -28,12 +30,20 @@ @RequiredArgsConstructor public class TemplateMapper { + public static final String REVIEWEE_NAME_PLACEHOLDER = "{revieweeName}"; + + private final TemplateRepository templateRepository; private final SectionRepository sectionRepository; private final QuestionRepository questionRepository; private final OptionGroupRepository optionGroupRepository; private final OptionItemRepository optionItemRepository; - public TemplateResponse mapToTemplateResponse(ReviewGroup reviewGroup, Template template) { + public TemplateResponse mapToTemplateResponse(ReviewGroup reviewGroup) { + Template template = templateRepository.findById(reviewGroup.getTemplateId()) + .orElseThrow(() -> new TemplateNotFoundByReviewGroupException( + reviewGroup.getId(), reviewGroup.getTemplateId() + )); + List sectionResponses = template.getSectionIds() .stream() .map(templateSection -> mapToSectionResponse(templateSection, reviewGroup)) @@ -62,7 +72,7 @@ private SectionResponse mapToSectionResponse(TemplateSection templateSection, Re section.getSectionName(), section.getVisibleType().name(), section.getOnSelectedOptionId(), - section.convertHeader("{revieweeName}", reviewGroup.getReviewee()), + section.convertHeader(REVIEWEE_NAME_PLACEHOLDER, reviewGroup.getReviewee()), questionResponses ); } @@ -79,11 +89,11 @@ private QuestionResponse mapToQuestionResponse(SectionQuestion sectionQuestion, return new QuestionResponse( question.getId(), question.isRequired(), - question.convertContent("{revieweeName}", reviewGroup.getReviewee()), + question.convertContent(REVIEWEE_NAME_PLACEHOLDER, reviewGroup.getReviewee()), question.getQuestionType().name(), optionGroupResponse, question.hasGuideline(), - question.convertGuideLine("{revieweeName}", reviewGroup.getReviewee()) + question.convertGuideLine(REVIEWEE_NAME_PLACEHOLDER, reviewGroup.getReviewee()) ); } diff --git a/backend/src/main/resources/secret b/backend/src/main/resources/secret deleted file mode 160000 index 9e15707bb..000000000 --- a/backend/src/main/resources/secret +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9e15707bb4d91435d6c460b09343d2fb6e819fe1 diff --git a/backend/src/test/java/reviewme/api/ApiTest.java b/backend/src/test/java/reviewme/api/ApiTest.java index a0bf49b5d..70c24a488 100644 --- a/backend/src/test/java/reviewme/api/ApiTest.java +++ b/backend/src/test/java/reviewme/api/ApiTest.java @@ -22,9 +22,9 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import reviewme.review.controller.ReviewController; -import reviewme.review.service.CreateReviewService; +import reviewme.review.service.ReviewRegisterService; import reviewme.review.service.ReviewDetailLookupService; -import reviewme.review.service.ReviewService; +import reviewme.review.service.ReviewListLookupService; import reviewme.reviewgroup.controller.ReviewGroupController; import reviewme.reviewgroup.service.ReviewGroupLookupService; import reviewme.reviewgroup.service.ReviewGroupService; @@ -42,7 +42,7 @@ public abstract class ApiTest { private MockMvcRequestSpecification spec; @MockBean - protected ReviewService reviewService; + protected ReviewListLookupService reviewListLookupService; @MockBean protected ReviewGroupService reviewGroupService; @@ -51,7 +51,7 @@ public abstract class ApiTest { protected TemplateService templateService; @MockBean - protected CreateReviewService createReviewService; + protected ReviewRegisterService reviewRegisterService; @MockBean protected ReviewDetailLookupService reviewDetailLookupService; diff --git a/backend/src/test/java/reviewme/api/ReviewApiTest.java b/backend/src/test/java/reviewme/api/ReviewApiTest.java index 1d276aae1..ea913b166 100644 --- a/backend/src/test/java/reviewme/api/ReviewApiTest.java +++ b/backend/src/test/java/reviewme/api/ReviewApiTest.java @@ -21,9 +21,9 @@ import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.request.ParameterDescriptor; import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException; -import reviewme.review.service.dto.request.CreateReviewRequest; -import reviewme.review.service.dto.response.list.ReceivedReviewCategoryResponse; -import reviewme.review.service.dto.response.list.ReceivedReviewResponse; +import reviewme.review.service.dto.request.ReviewRegisterRequest; +import reviewme.review.service.dto.response.list.ReviewCategoryResponse; +import reviewme.review.service.dto.response.list.ReviewListElementResponse; import reviewme.review.service.dto.response.list.ReceivedReviewsResponse; import reviewme.review.service.exception.ReviewGroupNotFoundByCodesException; @@ -47,7 +47,7 @@ class ReviewApiTest extends ApiTest { @Test void 리뷰를_등록한다() { - BDDMockito.given(createReviewService.createReview(any(CreateReviewRequest.class))) + BDDMockito.given(reviewRegisterService.registerReview(any(ReviewRegisterRequest.class))) .willReturn(1L); FieldDescriptor[] requestFieldDescriptors = { @@ -74,7 +74,7 @@ class ReviewApiTest extends ApiTest { @Test void 리뷰_그룹_코드가_올바르지_않은_경우_예외가_발생한다() { - BDDMockito.given(createReviewService.createReview(any(CreateReviewRequest.class))) + BDDMockito.given(reviewRegisterService.registerReview(any(ReviewRegisterRequest.class))) .willThrow(new ReviewGroupNotFoundByReviewRequestCodeException(anyString())); FieldDescriptor[] requestFieldDescriptors = { @@ -191,14 +191,14 @@ class ReviewApiTest extends ApiTest { @Test void 자신이_받은_리뷰_목록을_조회한다() { - List receivedReviews = List.of( - new ReceivedReviewResponse(1L, LocalDate.of(2024, 8, 1), "(리뷰 미리보기 1)", - List.of(new ReceivedReviewCategoryResponse(1L, "카테고리 1"))), - new ReceivedReviewResponse(2L, LocalDate.of(2024, 8, 2), "(리뷰 미리보기 2)", - List.of(new ReceivedReviewCategoryResponse(2L, "카테고리 2"))) + List receivedReviews = List.of( + new ReviewListElementResponse(1L, LocalDate.of(2024, 8, 1), "(리뷰 미리보기 1)", + List.of(new ReviewCategoryResponse(1L, "카테고리 1"))), + new ReviewListElementResponse(2L, LocalDate.of(2024, 8, 2), "(리뷰 미리보기 2)", + List.of(new ReviewCategoryResponse(2L, "카테고리 2"))) ); ReceivedReviewsResponse response = new ReceivedReviewsResponse("아루", "리뷰미", receivedReviews); - BDDMockito.given(reviewService.findReceivedReviews(anyString(), anyString())) + BDDMockito.given(reviewListLookupService.getReceivedReviews(anyString(), anyString())) .willReturn(response); HeaderDescriptor[] requestHeaderDescriptors = { @@ -238,7 +238,7 @@ class ReviewApiTest extends ApiTest { void 자신이_받은_리뷰_조회시_접근_코드가_올바르지_않은_경우_예외를_발생한다() { String reviewRequestCode = "43214321"; String groupAccessCode = "00001234"; - BDDMockito.given(reviewService.findReceivedReviews(reviewRequestCode, groupAccessCode)) + BDDMockito.given(reviewListLookupService.getReceivedReviews(reviewRequestCode, groupAccessCode)) .willThrow(new ReviewGroupNotFoundByCodesException(reviewRequestCode, groupAccessCode)); HeaderDescriptor[] requestHeaderDescriptors = { diff --git a/backend/src/test/java/reviewme/api/TemplateFixture.java b/backend/src/test/java/reviewme/api/TemplateFixture.java index 1cee386d0..a4a941985 100644 --- a/backend/src/test/java/reviewme/api/TemplateFixture.java +++ b/backend/src/test/java/reviewme/api/TemplateFixture.java @@ -7,7 +7,7 @@ import reviewme.review.service.dto.response.detail.OptionItemAnswerResponse; import reviewme.review.service.dto.response.detail.QuestionAnswerResponse; import reviewme.review.service.dto.response.detail.SectionAnswerResponse; -import reviewme.review.service.dto.response.detail.TemplateAnswerResponse; +import reviewme.review.service.dto.response.detail.ReviewDetailResponse; import reviewme.template.domain.VisibleType; import reviewme.template.service.dto.response.OptionGroupResponse; import reviewme.template.service.dto.response.OptionItemResponse; @@ -72,7 +72,7 @@ public static TemplateResponse templateResponse() { return new TemplateResponse(1, "아루", "리뷰미", List.of(firstSection, secondSection)); } - public static TemplateAnswerResponse templateAnswerResponse() { + public static ReviewDetailResponse templateAnswerResponse() { // Section 1 List firstOptionAnswers = List.of( new OptionItemAnswerResponse(1, "커뮤니케이션, 협업 능력 (ex: 팀원간의 원활한 정보 공유, 명확한 의사소통)", true), @@ -102,7 +102,7 @@ public static TemplateAnswerResponse templateAnswerResponse() { 2, "커뮤니케이션, 협업 능력에서 어떤 부분이 인상 깊었는지 선택해주세요.", List.of(secondQuestionAnswer) ); - return new TemplateAnswerResponse( + return new ReviewDetailResponse( 1, "아루", "리뷰미", LocalDate.of(2024, 8, 1), List.of(firstSectionAnswer, secondSectionAnswer) ); } diff --git a/backend/src/test/java/reviewme/review/domain/CheckboxAnswerTest.java b/backend/src/test/java/reviewme/review/domain/CheckboxAnswerTest.java new file mode 100644 index 000000000..4581690ab --- /dev/null +++ b/backend/src/test/java/reviewme/review/domain/CheckboxAnswerTest.java @@ -0,0 +1,22 @@ +package reviewme.review.domain; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.List; +import org.junit.jupiter.api.Test; +import reviewme.review.domain.exception.QuestionNotAnsweredException; + +class CheckboxAnswerTest { + + @Test + void 답변이_없는_경우_예외를_발생한다() { + // given, when, then + assertAll( + () -> assertThatThrownBy(() -> new CheckboxAnswer(1L, null)) + .isInstanceOf(QuestionNotAnsweredException.class), + () -> assertThatThrownBy(() -> new CheckboxAnswer(1L, List.of())) + .isInstanceOf(QuestionNotAnsweredException.class) + ); + } +} diff --git a/backend/src/test/java/reviewme/review/domain/ReviewTest.java b/backend/src/test/java/reviewme/review/domain/ReviewTest.java new file mode 100644 index 000000000..f75ba2ec0 --- /dev/null +++ b/backend/src/test/java/reviewme/review/domain/ReviewTest.java @@ -0,0 +1,60 @@ +package reviewme.review.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class ReviewTest { + + private static final Logger log = LoggerFactory.getLogger(ReviewTest.class); + + @Test + void 리뷰에_등록된_답변의_모든_질문들을_반환한다() { + // given + TextAnswer textAnswer = new TextAnswer(1L, "답변"); + CheckboxAnswer checkboxAnswer = new CheckboxAnswer(2L, List.of(1L)); + Review review = new Review(1L, 1L, List.of(textAnswer), List.of(checkboxAnswer)); + + // when + Set allQuestionIdsFromAnswers = review.getAnsweredQuestionIds(); + + // then + assertThat(allQuestionIdsFromAnswers).containsAll(List.of(1L, 2L)); + } + + @Test + void 리뷰에_등록된_모든_선택형_답변의_옵션들을_반환환다() { + // given + CheckboxAnswer checkboxAnswer1 = new CheckboxAnswer(1L, List.of(1L, 2L)); + CheckboxAnswer checkboxAnswer2 = new CheckboxAnswer(1L, List.of(3L, 4L)); + Review review = new Review(1L, 1L, List.of(), List.of(checkboxAnswer1, checkboxAnswer2)); + + // when + Set allQuestionIdsFromAnswers = review.getAllCheckBoxOptionIds(); + + // then + assertThat(allQuestionIdsFromAnswers).containsAll(List.of(1L, 2L, 3L, 4L)); + } + + @Test + void 리뷰에_특정_질문에_대한_답변이_있는지_여부를_반환한다() { + // given + long textQuestionId = 1L; + long checkBoxQuestionId = 2L; + + TextAnswer textAnswer = new TextAnswer(textQuestionId, "답변"); + CheckboxAnswer checkboxAnswer = new CheckboxAnswer(checkBoxQuestionId, List.of(1L)); + Review review = new Review(1L, 1L, List.of(textAnswer), List.of(checkboxAnswer)); + + // when, then + assertAll( + () -> assertThat(review.hasAnsweredQuestion(textQuestionId)).isTrue(), + () -> assertThat(review.hasAnsweredQuestion(checkBoxQuestionId)).isTrue() + ); + } +} diff --git a/backend/src/test/java/reviewme/review/domain/TextAnswerTest.java b/backend/src/test/java/reviewme/review/domain/TextAnswerTest.java new file mode 100644 index 000000000..bea1aa188 --- /dev/null +++ b/backend/src/test/java/reviewme/review/domain/TextAnswerTest.java @@ -0,0 +1,21 @@ +package reviewme.review.domain; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.junit.jupiter.api.Test; +import reviewme.review.domain.exception.QuestionNotAnsweredException; + +class TextAnswerTest { + + @Test + void 답변이_없는_경우_예외를_발생한다() { + // given, when, then + assertAll( + () -> assertThatThrownBy(() -> new TextAnswer(1L, null)) + .isInstanceOf(QuestionNotAnsweredException.class), + () -> assertThatThrownBy(() -> new TextAnswer(1L, "")) + .isInstanceOf(QuestionNotAnsweredException.class) + ); + } +} diff --git a/backend/src/test/java/reviewme/review/service/CreateCheckBoxAnswerRequestValidatorTest.java b/backend/src/test/java/reviewme/review/service/CreateCheckBoxAnswerRequestValidatorTest.java deleted file mode 100644 index d450e3ebc..000000000 --- a/backend/src/test/java/reviewme/review/service/CreateCheckBoxAnswerRequestValidatorTest.java +++ /dev/null @@ -1,157 +0,0 @@ -package reviewme.review.service; - -import static org.assertj.core.api.Assertions.assertThatCode; -import static reviewme.fixture.QuestionFixture.선택형_필수_질문; - -import java.util.List; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import reviewme.fixture.OptionGroupFixture; -import reviewme.fixture.OptionItemFixture; -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.service.dto.request.CreateReviewAnswerRequest; -import reviewme.review.service.exception.CheckBoxAnswerIncludedNotProvidedOptionItemException; -import reviewme.review.service.exception.CheckBoxAnswerIncludedTextException; -import reviewme.review.service.exception.RequiredQuestionNotAnsweredException; -import reviewme.review.service.exception.SelectedOptionItemCountOutOfRangeException; -import reviewme.review.service.exception.SubmittedQuestionNotFoundException; -import reviewme.support.ServiceTest; -import reviewme.template.domain.exception.OptionGroupNotFoundByQuestionIdException; - -@ServiceTest -class CreateCheckBoxAnswerRequestValidatorTest { - - @Autowired - private CreateCheckBoxAnswerRequestValidator createCheckBoxAnswerRequestValidator; - - @Autowired - private QuestionRepository questionRepository; - - @Autowired - private OptionGroupRepository optionGroupRepository; - - @Autowired - private OptionItemRepository optionItemRepository; - - @Test - void 저장되지_않은_질문에_대한_응답이면_예외가_발생한다() { - // given - long notSavedQuestionId = 100L; - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest( - notSavedQuestionId, List.of(1L), null - ); - - // when, then - assertThatCode(() -> createCheckBoxAnswerRequestValidator.validate(request)) - .isInstanceOf(SubmittedQuestionNotFoundException.class); - } - - @Test - void 선택형_질문에_텍스트_응답을_하면_예외가_발생한다() { - // given - Question savedQuestion = questionRepository.save(선택형_필수_질문()); - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest( - savedQuestion.getId(), List.of(1L), "서술형 응답" - ); - - // when, then - assertThatCode(() -> createCheckBoxAnswerRequestValidator.validate(request)) - .isInstanceOf(CheckBoxAnswerIncludedTextException.class); - } - - @Test - void 응답한_질문과_대응하는_옵션그룹이_존재하지_않으면_예외가_발생한다() { - // given - long notselectedOptionId = 1L; - Question savedQuestion = questionRepository.save(선택형_필수_질문()); - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest( - savedQuestion.getId(), List.of(notselectedOptionId), null - ); - - // when, then - assertThatCode(() -> createCheckBoxAnswerRequestValidator.validate(request)) - .isInstanceOf(OptionGroupNotFoundByQuestionIdException.class); - } - - @Test - void 필수_선택형_질문에_응답을_하지_않으면_예외가_발생한다() { - // given - Question savedQuestion = questionRepository.save(선택형_필수_질문()); - optionGroupRepository.save( - new OptionGroup(savedQuestion.getId(), 1, 3) - ); - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest( - savedQuestion.getId(), - null, - null); - - // when, then - assertThatCode(() -> createCheckBoxAnswerRequestValidator.validate(request)) - .isInstanceOf(RequiredQuestionNotAnsweredException.class); - } - - @Test - void 옵션그룹에서_제공하지_않은_옵션아이템을_응답하면_예외가_발생한다() { - // given - Question savedQuestion = questionRepository.save(선택형_필수_질문()); - OptionGroup savedOptionGroup = optionGroupRepository.save(OptionGroupFixture.선택지_그룹(savedQuestion.getId())); - OptionItem savedOptionItem = optionItemRepository.save(OptionItemFixture.선택지(savedOptionGroup.getId())); - - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest( - savedQuestion.getId(), List.of(savedOptionItem.getId() + 1L), null - ); - - // when, then - assertThatCode(() -> createCheckBoxAnswerRequestValidator.validate(request)) - .isInstanceOf(CheckBoxAnswerIncludedNotProvidedOptionItemException.class); - } - - @Test - void 옵션그룹에서_정한_최소_선택_수_보다_적게_선택하면_예외가_발생한다() { - // given - Question savedQuestion = questionRepository.save(선택형_필수_질문()); - OptionGroup savedOptionGroup = optionGroupRepository.save( - new OptionGroup(savedQuestion.getId(), 2, 3) - ); - OptionItem savedOptionItem1 = optionItemRepository.save( - new OptionItem("옵션1", savedOptionGroup.getId(), 1, OptionType.KEYWORD) - ); - - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest( - savedQuestion.getId(), List.of(savedOptionItem1.getId()), null - ); - - // when, then - assertThatCode(() -> createCheckBoxAnswerRequestValidator.validate(request)) - .isInstanceOf(SelectedOptionItemCountOutOfRangeException.class); - } - - @Test - void 옵션그룹에서_정한_최대_선택_수_보다_많이_선택하면_예외가_발생한다() { - // given - Question savedQuestion = questionRepository.save(선택형_필수_질문()); - OptionGroup savedOptionGroup = optionGroupRepository.save( - new OptionGroup(savedQuestion.getId(), 1, 1) - ); - OptionItem savedOptionItem1 = optionItemRepository.save( - new OptionItem("옵션1", savedOptionGroup.getId(), 1, OptionType.KEYWORD) - ); - OptionItem savedOptionItem2 = optionItemRepository.save( - new OptionItem("옵션2", savedOptionGroup.getId(), 2, OptionType.KEYWORD) - ); - - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest( - savedQuestion.getId(), List.of(savedOptionItem1.getId(), savedOptionItem2.getId()), null - ); - - // when, then - assertThatCode(() -> createCheckBoxAnswerRequestValidator.validate(request)) - .isInstanceOf(SelectedOptionItemCountOutOfRangeException.class); - } -} diff --git a/backend/src/test/java/reviewme/review/service/CreateReviewServiceTest.java b/backend/src/test/java/reviewme/review/service/CreateReviewServiceTest.java deleted file mode 100644 index 628c14163..000000000 --- a/backend/src/test/java/reviewme/review/service/CreateReviewServiceTest.java +++ /dev/null @@ -1,239 +0,0 @@ -package reviewme.review.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -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.ReviewGroupFixture.리뷰_그룹; -import static reviewme.fixture.SectionFixture.조건부로_보이는_섹션; -import static reviewme.fixture.SectionFixture.항상_보이는_섹션; -import static reviewme.fixture.TemplateFixture.템플릿; - -import java.util.List; -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.repository.CheckboxAnswerRepository; -import reviewme.review.repository.ReviewRepository; -import reviewme.review.repository.TextAnswerRepository; -import reviewme.review.service.dto.request.CreateReviewAnswerRequest; -import reviewme.review.service.dto.request.CreateReviewRequest; -import reviewme.review.service.exception.MissingRequiredQuestionException; -import reviewme.review.service.exception.UnnecessaryQuestionIncludedException; -import reviewme.reviewgroup.repository.ReviewGroupRepository; -import reviewme.support.ServiceTest; -import reviewme.template.domain.Section; -import reviewme.template.repository.SectionRepository; -import reviewme.template.repository.TemplateRepository; - -@ServiceTest -class CreateReviewServiceTest { - - @Autowired - private CreateReviewService createReviewService; - - @Autowired - private QuestionRepository questionRepository; - - @Autowired - private OptionGroupRepository optionGroupRepository; - - @Autowired - private OptionItemRepository optionItemRepository; - - @Autowired - private ReviewGroupRepository reviewGroupRepository; - - @Autowired - private TemplateRepository templateRepository; - - @Autowired - private ReviewRepository reviewRepository; - - @Autowired - private SectionRepository sectionRepository; - - @Autowired - private TextAnswerRepository textAnswerRepository; - - @Autowired - private CheckboxAnswerRepository checkboxAnswerRepository; - - @Test - void 필수_질문에_모두_응답하는_경우_예외가_발생하지_않는다() { - // 리뷰 그룹 저장 - String reviewRequestCode = "1234"; - reviewGroupRepository.save(리뷰_그룹(reviewRequestCode, "12341234")); - - // 필수 선택형 질문, 섹션 저장 - Question alwaysRequiredQuestion = questionRepository.save(선택형_필수_질문()); - OptionGroup alwaysRequiredOptionGroup = optionGroupRepository.save(선택지_그룹(alwaysRequiredQuestion.getId())); - OptionItem alwaysRequiredOptionItem1 = optionItemRepository.save(선택지(alwaysRequiredOptionGroup.getId(), 1)); - OptionItem alwaysRequiredOptionItem2 = optionItemRepository.save(선택지(alwaysRequiredOptionGroup.getId(), 2)); - Section alwaysRequiredSection = sectionRepository.save(항상_보이는_섹션(List.of(alwaysRequiredQuestion.getId()))); - - // 필수가 아닌 서술형 질문 저장 - Question notRequiredQuestion = questionRepository.save(서술형_옵션_질문()); - Section notRequiredSection = sectionRepository.save(항상_보이는_섹션(List.of(notRequiredQuestion.getId()), 1)); - - // optionItem 선택에 따라서 required 가 달라지는 섹션1 저장 - Question conditionalTextQuestion1 = questionRepository.save(서술형_필수_질문(1)); - Question conditionalCheckQuestion = questionRepository.save(선택형_필수_질문(2)); - OptionGroup conditionalOptionGroup = optionGroupRepository.save(선택지_그룹(conditionalCheckQuestion.getId())); - OptionItem conditionalOptionItem = optionItemRepository.save(선택지(conditionalOptionGroup.getId())); - Section conditionalSection1 = sectionRepository.save( - 조건부로_보이는_섹션(List.of(conditionalTextQuestion1.getId(), conditionalCheckQuestion.getId()), - alwaysRequiredOptionItem1.getId(), 2)); - - // optionItem 선택에 따라서 required 가 달라지는 섹션2 저장 - Question conditionalQuestion2 = questionRepository.save(서술형_필수_질문()); - Section conditionalSection2 = sectionRepository.save( - 조건부로_보이는_섹션(List.of(conditionalQuestion2.getId()), alwaysRequiredOptionItem2.getId(), 3) - ); - - // 템플릿 저장 - templateRepository.save(템플릿( - List.of(alwaysRequiredSection.getId(), conditionalSection1.getId(), - conditionalSection2.getId(), notRequiredSection.getId()) - )); - - // 각 질문에 대한 답변 생성 - CreateReviewAnswerRequest alwaysRequiredAnswer = new CreateReviewAnswerRequest( - alwaysRequiredQuestion.getId(), List.of(alwaysRequiredOptionItem1.getId()), null); - CreateReviewAnswerRequest conditionalTextAnswer1 = new CreateReviewAnswerRequest( - conditionalTextQuestion1.getId(), null, "답변".repeat(30)); - CreateReviewAnswerRequest conditionalCheckAnswer1 = new CreateReviewAnswerRequest( - conditionalCheckQuestion.getId(), List.of(conditionalOptionItem.getId()), null); - CreateReviewAnswerRequest conditionalTextAnswer2 = new CreateReviewAnswerRequest( - conditionalQuestion2.getId(), null, "답변".repeat(30)); - - // 상황별로 다르게 구성한 리뷰 생성 dto - CreateReviewRequest properRequest = new CreateReviewRequest( - reviewRequestCode, List.of(alwaysRequiredAnswer, conditionalTextAnswer1, conditionalCheckAnswer1)); - CreateReviewRequest selectedOptionIdQuestionMissingRequest1 = new CreateReviewRequest( - reviewRequestCode, List.of(alwaysRequiredAnswer)); - CreateReviewRequest selectedOptionIdQuestionMissingRequest2 = new CreateReviewRequest( - reviewRequestCode, List.of(alwaysRequiredAnswer, conditionalTextAnswer1)); - CreateReviewRequest selectedOptionIdQuestionMissingRequest3 = new CreateReviewRequest( - reviewRequestCode, List.of(alwaysRequiredAnswer, conditionalCheckAnswer1)); - CreateReviewRequest unnecessaryQuestionIncludedRequest = new CreateReviewRequest( - reviewRequestCode, List.of(alwaysRequiredAnswer, conditionalTextAnswer1, - conditionalCheckAnswer1, conditionalTextAnswer2)); - - // when, then - assertThatCode(() -> createReviewService.createReview(properRequest)) - .doesNotThrowAnyException(); - assertThatCode(() -> createReviewService.createReview(selectedOptionIdQuestionMissingRequest1)) - .isInstanceOf(MissingRequiredQuestionException.class); - assertThatCode(() -> createReviewService.createReview(selectedOptionIdQuestionMissingRequest2)) - .isInstanceOf(MissingRequiredQuestionException.class); - assertThatCode(() -> createReviewService.createReview(selectedOptionIdQuestionMissingRequest3)) - .isInstanceOf(MissingRequiredQuestionException.class); - assertThatCode(() -> createReviewService.createReview(unnecessaryQuestionIncludedRequest)) - .isInstanceOf(UnnecessaryQuestionIncludedException.class); - } - - @Test - void 텍스트가_포함된_리뷰를_저장한다() { - // given - String reviewRequestCode = "0000"; - reviewGroupRepository.save(리뷰_그룹(reviewRequestCode, "12341234")); - Section section = sectionRepository.save(항상_보이는_섹션(List.of(1L))); - templateRepository.save(템플릿(List.of(section.getId()))); - - String expectedTextAnswer = "답".repeat(20); - Question savedQuestion = questionRepository.save(서술형_필수_질문()); - CreateReviewAnswerRequest createReviewAnswerRequest = new CreateReviewAnswerRequest(savedQuestion.getId(), null, - expectedTextAnswer); - CreateReviewRequest createReviewRequest = new CreateReviewRequest(reviewRequestCode, - List.of(createReviewAnswerRequest)); - - // when - createReviewService.createReview(createReviewRequest); - - // then - assertThat(reviewRepository.findAll()).hasSize(1); - assertThat(textAnswerRepository.findAll()).hasSize(1); - } - - @Test - void 필수가_아닌_텍스트형_응답에_빈문자열이_들어오면_저장하지_않는다() { - // given - String reviewRequestCode = "0000"; - reviewGroupRepository.save(리뷰_그룹(reviewRequestCode, "12341234")); - Section section = sectionRepository.save(항상_보이는_섹션(List.of(1L))); - templateRepository.save(템플릿(List.of(section.getId()))); - - Question savedQuestion = questionRepository.save(서술형_옵션_질문()); - CreateReviewAnswerRequest emptyTextReviewRequest = new CreateReviewAnswerRequest( - savedQuestion.getId(), null, ""); - CreateReviewAnswerRequest validTextReviewRequest = new CreateReviewAnswerRequest( - savedQuestion.getId(), null, "질문 1 답변 (20자 이상 입력 적용)"); - CreateReviewRequest createReviewRequest = new CreateReviewRequest(reviewRequestCode, - List.of(emptyTextReviewRequest, validTextReviewRequest)); - - // when - createReviewService.createReview(createReviewRequest); - - // then - assertThat(reviewRepository.findAll()).hasSize(1); - assertThat(textAnswerRepository.findAll()).hasSize(1); - } - - @Test - void 체크박스가_포함된_리뷰를_저장한다() { - // given - String reviewRequestCode = "0000"; - reviewGroupRepository.save(리뷰_그룹(reviewRequestCode, "12341234")); - Section section = sectionRepository.save(항상_보이는_섹션(List.of(1L))); - templateRepository.save(템플릿(List.of(section.getId()))); - - Question savedQuestion = questionRepository.save(선택형_필수_질문()); - OptionGroup savedOptionGroup = optionGroupRepository.save(선택지_그룹(savedQuestion.getId())); - OptionItem savedOptionItem1 = optionItemRepository.save(선택지(savedOptionGroup.getId(), 1)); - OptionItem savedOptionItem2 = optionItemRepository.save(선택지(savedOptionGroup.getId(), 2)); - CreateReviewAnswerRequest createReviewAnswerRequest = new CreateReviewAnswerRequest(savedQuestion.getId(), - List.of(savedOptionItem1.getId(), savedOptionItem2.getId()), null); - CreateReviewRequest createReviewRequest = new CreateReviewRequest(reviewRequestCode, - List.of(createReviewAnswerRequest)); - - // when - createReviewService.createReview(createReviewRequest); - - // then - assertThat(reviewRepository.findAll()).hasSize(1); - assertThat(checkboxAnswerRepository.findAll()).hasSize(1); - } - - @Test - void 적정_글자수인_텍스트_응답인_경우_정상_저장된다() { - // given - String reviewRequestCode = "0000"; - reviewGroupRepository.save(리뷰_그룹(reviewRequestCode, "12341234")); - Section section = sectionRepository.save(항상_보이는_섹션(List.of(1L))); - templateRepository.save(템플릿(List.of(section.getId()))); - - String expectedTextAnswer = "답".repeat(1000); - Question savedQuestion = questionRepository.save(서술형_필수_질문()); - - CreateReviewAnswerRequest createReviewAnswerRequest = new CreateReviewAnswerRequest(savedQuestion.getId(), null, - expectedTextAnswer); - CreateReviewRequest createReviewRequest = new CreateReviewRequest(reviewRequestCode, - List.of(createReviewAnswerRequest)); - - // when - createReviewService.createReview(createReviewRequest); - - // then - assertThat(reviewRepository.findAll()).hasSize(1); - assertThat(textAnswerRepository.findAll()).hasSize(1); - } -} diff --git a/backend/src/test/java/reviewme/review/service/CreateTextAnswerRequestValidatorTest.java b/backend/src/test/java/reviewme/review/service/CreateTextAnswerRequestValidatorTest.java deleted file mode 100644 index 8224d4acd..000000000 --- a/backend/src/test/java/reviewme/review/service/CreateTextAnswerRequestValidatorTest.java +++ /dev/null @@ -1,77 +0,0 @@ -package reviewme.review.service; - -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.util.List; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; -import reviewme.question.domain.Question; -import reviewme.question.domain.QuestionType; -import reviewme.question.repository.QuestionRepository; -import reviewme.review.domain.exception.InvalidTextAnswerLengthException; -import reviewme.review.service.dto.request.CreateReviewAnswerRequest; -import reviewme.review.service.exception.RequiredQuestionNotAnsweredException; -import reviewme.review.service.exception.SubmittedQuestionNotFoundException; -import reviewme.review.service.exception.TextAnswerIncludedOptionItemException; -import reviewme.support.ServiceTest; - -@ServiceTest -class CreateTextAnswerRequestValidatorTest { - - @Autowired - private CreateTextAnswerRequestValidator createTextAnswerRequestValidator; - - @Autowired - private QuestionRepository questionRepository; - - @Test - void 저장되지_않은_질문에_대한_대답이면_예외가_발생한다() { - // given - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest(100L, null, "텍스트형 응답"); - - // when, then - assertThatCode(() -> createTextAnswerRequestValidator.validate(request)) - .isInstanceOf(SubmittedQuestionNotFoundException.class); - } - - @Test - void 텍스트형_질문에_선택형_응답을_하면_예외가_발생한다() { - // given - Question savedQuestion - = questionRepository.save(new Question(true, QuestionType.TEXT, "질문", "가이드라인", 1)); - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest(savedQuestion.getId(), List.of(1L), "응답"); - - // when, then - assertThatCode(() -> createTextAnswerRequestValidator.validate(request)) - .isInstanceOf(TextAnswerIncludedOptionItemException.class); - } - - @Test - void 필수_텍스트형_질문에_응답을_하지_않으면_예외가_발생한다() { - // given - Question savedQuestion - = questionRepository.save(new Question(true, QuestionType.TEXT, "질문", "가이드라인", 1)); - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest(savedQuestion.getId(), null, null); - - // when, then - assertThatCode(() -> createTextAnswerRequestValidator.validate(request)) - .isInstanceOf(RequiredQuestionNotAnsweredException.class); - } - - @ParameterizedTest - @ValueSource(ints = {19, 10001}) - void 답변_길이가_유효하지_않으면_예외가_발생한다(int length) { - // given - String textAnswer = "답".repeat(length); - Question savedQuestion - = questionRepository.save(new Question(true, QuestionType.TEXT, "질문", "가이드라인", 1)); - CreateReviewAnswerRequest request = new CreateReviewAnswerRequest(savedQuestion.getId(), null, textAnswer); - - // when, then - assertThatThrownBy(() -> createTextAnswerRequestValidator.validate(request)) - .isInstanceOf(InvalidTextAnswerLengthException.class); - } -} diff --git a/backend/src/test/java/reviewme/review/service/ReviewDetailLookupServiceTest.java b/backend/src/test/java/reviewme/review/service/ReviewDetailLookupServiceTest.java index edce59b50..8946db836 100644 --- a/backend/src/test/java/reviewme/review/service/ReviewDetailLookupServiceTest.java +++ b/backend/src/test/java/reviewme/review/service/ReviewDetailLookupServiceTest.java @@ -28,8 +28,8 @@ import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException; import reviewme.review.repository.ReviewRepository; import reviewme.review.service.dto.response.detail.QuestionAnswerResponse; +import reviewme.review.service.dto.response.detail.ReviewDetailResponse; import reviewme.review.service.dto.response.detail.SectionAnswerResponse; -import reviewme.review.service.dto.response.detail.TemplateAnswerResponse; import reviewme.review.service.exception.ReviewGroupUnauthorizedException; import reviewme.review.service.exception.ReviewNotFoundByIdAndGroupException; import reviewme.reviewgroup.domain.ReviewGroup; @@ -148,7 +148,7 @@ class ReviewDetailLookupServiceTest { ); // when - TemplateAnswerResponse reviewDetail = reviewDetailLookupService.getReviewDetail( + ReviewDetailResponse reviewDetail = reviewDetailLookupService.getReviewDetail( review.getId(), reviewRequestCode, groupAccessCode ); @@ -177,7 +177,7 @@ class 필수가_아닌_답변에_응답하지_않았을_때 { ); // when - TemplateAnswerResponse reviewDetail = reviewDetailLookupService.getReviewDetail( + ReviewDetailResponse reviewDetail = reviewDetailLookupService.getReviewDetail( review.getId(), reviewRequestCode, groupAccessCode ); @@ -207,7 +207,7 @@ class 필수가_아닌_답변에_응답하지_않았을_때 { ); // when - TemplateAnswerResponse reviewDetail = reviewDetailLookupService.getReviewDetail( + ReviewDetailResponse reviewDetail = reviewDetailLookupService.getReviewDetail( review.getId(), reviewRequestCode, groupAccessCode ); diff --git a/backend/src/test/java/reviewme/review/service/ReviewServiceTest.java b/backend/src/test/java/reviewme/review/service/ReviewListLookupServiceTest.java similarity index 88% rename from backend/src/test/java/reviewme/review/service/ReviewServiceTest.java rename to backend/src/test/java/reviewme/review/service/ReviewListLookupServiceTest.java index 59edcb9c3..91ef8204c 100644 --- a/backend/src/test/java/reviewme/review/service/ReviewServiceTest.java +++ b/backend/src/test/java/reviewme/review/service/ReviewListLookupServiceTest.java @@ -34,10 +34,10 @@ import reviewme.template.repository.TemplateRepository; @ServiceTest -class ReviewServiceTest { +class ReviewListLookupServiceTest { @Autowired - private ReviewService reviewService; + private ReviewListLookupService reviewListLookupService; @Autowired private QuestionRepository questionRepository; @@ -62,7 +62,7 @@ class ReviewServiceTest { @Test void 리뷰_요청_코드가_존재하지_않는_경우_예외가_발생한다() { - assertThatThrownBy(() -> reviewService.findReceivedReviews("abc", "groupAccessCode")) + assertThatThrownBy(() -> reviewListLookupService.getReceivedReviews("abc", "groupAccessCode")) .isInstanceOf(ReviewGroupNotFoundByReviewRequestCodeException.class); } @@ -74,9 +74,9 @@ class ReviewServiceTest { ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹(reviewRequestCode, groupAccessCode)); // when, then - assertThatThrownBy(() -> reviewService.findReceivedReviews( - reviewRequestCode, "wrong" + groupAccessCode - )).isInstanceOf(ReviewGroupUnauthorizedException.class); + assertThatThrownBy(() -> reviewListLookupService.getReceivedReviews( + reviewRequestCode, "wrong" + groupAccessCode)) + .isInstanceOf(ReviewGroupUnauthorizedException.class); } @Test @@ -103,7 +103,8 @@ class ReviewServiceTest { reviewRepository.saveAll(List.of(review1, review2)); // when - ReceivedReviewsResponse response = reviewService.findReceivedReviews(reviewRequestCode, groupAccessCode); + ReceivedReviewsResponse response = reviewListLookupService.getReceivedReviews(reviewRequestCode, + groupAccessCode); // then assertThat(response.reviews()).hasSize(2); diff --git a/backend/src/test/java/reviewme/review/service/ReviewRegisterServiceTest.java b/backend/src/test/java/reviewme/review/service/ReviewRegisterServiceTest.java new file mode 100644 index 000000000..246fe2774 --- /dev/null +++ b/backend/src/test/java/reviewme/review/service/ReviewRegisterServiceTest.java @@ -0,0 +1,108 @@ +package reviewme.review.service; + +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 static reviewme.fixture.ReviewGroupFixture.리뷰_그룹; +import static reviewme.fixture.SectionFixture.조건부로_보이는_섹션; +import static reviewme.fixture.SectionFixture.항상_보이는_섹션; +import static reviewme.fixture.TemplateFixture.템플릿; + +import java.util.List; +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.request.ReviewAnswerRequest; +import reviewme.review.service.dto.request.ReviewRegisterRequest; +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 ReviewRegisterServiceTest { + + @Autowired + private ReviewRegisterService reviewRegisterService; + + @Autowired + private QuestionRepository questionRepository; + + @Autowired + private OptionGroupRepository optionGroupRepository; + + @Autowired + private OptionItemRepository optionItemRepository; + + @Autowired + private ReviewGroupRepository reviewGroupRepository; + + @Autowired + private TemplateRepository templateRepository; + + @Autowired + private ReviewRepository reviewRepository; + + @Autowired + private SectionRepository sectionRepository; + + @Test + void 요청한_내용으로_리뷰를_등록한다() { + // given + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + Question requiredCheckQuestion = questionRepository.save(선택형_필수_질문()); + OptionGroup requiredOptionGroup = optionGroupRepository.save(선택지_그룹(requiredCheckQuestion.getId())); + OptionItem requiredOptionItem1 = optionItemRepository.save(선택지(requiredOptionGroup.getId())); + OptionItem requiredOptionItem2 = optionItemRepository.save(선택지(requiredOptionGroup.getId())); + Section visibleSection = sectionRepository.save(항상_보이는_섹션(List.of(requiredCheckQuestion.getId()), 1)); + + Question requiredTextQuestion = questionRepository.save(서술형_필수_질문()); + Question conditionalCheckQuestion = questionRepository.save(선택형_필수_질문()); + OptionGroup conditionalOptionGroup = optionGroupRepository.save(선택지_그룹(conditionalCheckQuestion.getId())); + OptionItem conditionalOptionItem1 = optionItemRepository.save(선택지(conditionalOptionGroup.getId())); + OptionItem conditionalOptionItem2 = optionItemRepository.save(선택지(conditionalOptionGroup.getId())); + Section conditionalSection = sectionRepository.save(조건부로_보이는_섹션( + List.of(requiredCheckQuestion.getId(), requiredTextQuestion.getId(), conditionalCheckQuestion.getId()), + requiredOptionItem1.getId(), 2) + ); + + Template template = templateRepository.save(템플릿(List.of(visibleSection.getId(), conditionalSection.getId()))); + + ReviewAnswerRequest requiredCheckQuestionAnswer = new ReviewAnswerRequest( + requiredCheckQuestion.getId(), List.of(requiredOptionItem1.getId()), null); + ReviewAnswerRequest requiredTextQuestionAnswer = new ReviewAnswerRequest( + requiredTextQuestion.getId(), null, "답변".repeat(30)); + ReviewAnswerRequest conditionalCheckQuestionAnswer = new ReviewAnswerRequest( + conditionalCheckQuestion.getId(), List.of(conditionalOptionItem1.getId()), null); + ReviewRegisterRequest reviewRegisterRequest = new ReviewRegisterRequest(reviewGroup.getReviewRequestCode(), + List.of(requiredCheckQuestionAnswer, requiredTextQuestionAnswer, conditionalCheckQuestionAnswer)); + + // when + long registeredReviewId = reviewRegisterService.registerReview(reviewRegisterRequest); + + // when, then + Review review = reviewRepository.findById(registeredReviewId).orElseThrow(); + assertAll( + () -> assertThat(review.getTextAnswers()).extracting(TextAnswer::getQuestionId) + .containsExactly(requiredTextQuestion.getId()), + () -> assertThat(review.getCheckboxAnswers()).extracting(CheckboxAnswer::getQuestionId) + .containsAll(List.of(requiredCheckQuestion.getId(), conditionalCheckQuestion.getId())) + ); + } +} diff --git a/backend/src/test/java/reviewme/review/service/module/AnswerMapperTest.java b/backend/src/test/java/reviewme/review/service/module/AnswerMapperTest.java new file mode 100644 index 000000000..b7b30f2e5 --- /dev/null +++ b/backend/src/test/java/reviewme/review/service/module/AnswerMapperTest.java @@ -0,0 +1,85 @@ +package reviewme.review.service.module; + +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 java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import reviewme.review.domain.CheckBoxAnswerSelectedOption; +import reviewme.review.domain.CheckboxAnswer; +import reviewme.review.domain.TextAnswer; +import reviewme.review.service.dto.request.ReviewAnswerRequest; +import reviewme.review.service.exception.CheckBoxAnswerIncludedTextException; +import reviewme.review.service.exception.TextAnswerIncludedOptionItemException; +import reviewme.support.ServiceTest; + +@ServiceTest +class AnswerMapperTest { + + @Autowired + private AnswerMapper answerMapper; + + @Test + void 답변_요청을_서술형_답변으로_매핑한다() { + // given + long questionId = 1L; + String text = "답변"; + ReviewAnswerRequest answerRequest = new ReviewAnswerRequest(questionId, null, text); + + // when + TextAnswer textAnswer = answerMapper.mapToTextAnswer(answerRequest); + + // then + assertAll( + () -> assertThat(textAnswer.getQuestionId()).isEqualTo(questionId), + () -> assertThat(textAnswer.getContent()).isEqualTo(text) + ); + } + + @Test + void 답변_요청을_선택형_답변으로_매핑한다() { + // given + long questionId = 1L; + long selectedOptionsId = 2L; + ReviewAnswerRequest answerRequest = new ReviewAnswerRequest(questionId, List.of(selectedOptionsId), null); + + // when + CheckboxAnswer checkboxAnswer = answerMapper.mapToCheckBoxAnswer(answerRequest); + + // then + assertAll( + () -> assertThat(checkboxAnswer.getQuestionId()).isEqualTo(questionId), + () -> assertThat(checkboxAnswer.getSelectedOptionIds()) + .extracting(CheckBoxAnswerSelectedOption::getSelectedOptionId) + .containsOnly(selectedOptionsId) + ); + } + + @Test + void 서술형_답변_매핑시_선택형_답변이_존재할_경우_예외가_발생한다() { + // given + long questionId = 1L; + String text = "답변"; + long selectedOptionsId = 2L; + ReviewAnswerRequest answerRequest = new ReviewAnswerRequest(questionId, List.of(selectedOptionsId), text); + + // when, then + assertThatThrownBy(() -> answerMapper.mapToTextAnswer(answerRequest)) + .isInstanceOf(TextAnswerIncludedOptionItemException.class); + } + + @Test + void 선택형_답변_매핑시_서술형_답변이_존재할_경우_예외가_발생한다() { + // given + long questionId = 1L; + String text = "답변"; + long selectedOptionsId = 2L; + ReviewAnswerRequest answerRequest = new ReviewAnswerRequest(questionId, List.of(selectedOptionsId), text); + + // when, then + assertThatThrownBy(() -> answerMapper.mapToCheckBoxAnswer(answerRequest)) + .isInstanceOf(CheckBoxAnswerIncludedTextException.class); + } +} diff --git a/backend/src/test/java/reviewme/review/service/module/CheckBoxAnswerValidatorTest.java b/backend/src/test/java/reviewme/review/service/module/CheckBoxAnswerValidatorTest.java new file mode 100644 index 000000000..c1cc63151 --- /dev/null +++ b/backend/src/test/java/reviewme/review/service/module/CheckBoxAnswerValidatorTest.java @@ -0,0 +1,109 @@ +package reviewme.review.service.module; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static reviewme.fixture.OptionGroupFixture.선택지_그룹; +import static reviewme.fixture.OptionItemFixture.선택지; +import static reviewme.fixture.QuestionFixture.선택형_필수_질문; + +import java.util.List; +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.service.exception.CheckBoxAnswerIncludedNotProvidedOptionItemException; +import reviewme.review.service.exception.SelectedOptionItemCountOutOfRangeException; +import reviewme.review.service.exception.SubmittedQuestionNotFoundException; +import reviewme.support.ServiceTest; +import reviewme.template.domain.exception.OptionGroupNotFoundByQuestionIdException; + +@ServiceTest +class CheckBoxAnswerValidatorTest { + + @Autowired + private CheckBoxAnswerValidator checkBoxAnswerValidator; + + @Autowired + private QuestionRepository questionRepository; + + @Autowired + private OptionGroupRepository optionGroupRepository; + + @Autowired + private OptionItemRepository optionItemRepository; + + @Test + void 저장되지_않은_질문에_대한_답변이면_예외가_발생한다() { + // given + long notSavedQuestionId = 100L; + CheckboxAnswer checkboxAnswer = new CheckboxAnswer(notSavedQuestionId, List.of(1L)); + + // when, then + assertThatCode(() -> checkBoxAnswerValidator.validate(checkboxAnswer)) + .isInstanceOf(SubmittedQuestionNotFoundException.class); + } + + @Test + void 옵션_그룹이_지정되지_않은_질문에_대한_답변이면_예외가_발생한다() { + // given + Question savedQuestion = questionRepository.save(선택형_필수_질문()); + CheckboxAnswer checkboxAnswer = new CheckboxAnswer(savedQuestion.getId(), List.of(1L)); + + // when, then + assertThatCode(() -> checkBoxAnswerValidator.validate(checkboxAnswer)) + .isInstanceOf(OptionGroupNotFoundByQuestionIdException.class); + } + + @Test + void 옵션그룹에서_제공하지_않은_옵션아이템을_응답하면_예외가_발생한다() { + // given + Question savedQuestion = questionRepository.save(선택형_필수_질문()); + OptionGroup savedOptionGroup = optionGroupRepository.save(선택지_그룹(savedQuestion.getId())); + OptionItem savedOptionItem = optionItemRepository.save(선택지(savedOptionGroup.getId())); + + CheckboxAnswer checkboxAnswer = new CheckboxAnswer(savedQuestion.getId(), + List.of(savedOptionItem.getId() + 1L)); + + // when, then + assertThatCode(() -> checkBoxAnswerValidator.validate(checkboxAnswer)) + .isInstanceOf(CheckBoxAnswerIncludedNotProvidedOptionItemException.class); + } + + @Test + void 옵션그룹에서_정한_최소_선택_수_보다_적게_선택하면_예외가_발생한다() { + // given + Question savedQuestion = questionRepository.save(선택형_필수_질문()); + OptionGroup savedOptionGroup = optionGroupRepository.save( + new OptionGroup(savedQuestion.getId(), 2, 3) + ); + OptionItem savedOptionItem1 = optionItemRepository.save(선택지(savedOptionGroup.getId())); + + CheckboxAnswer checkboxAnswer = new CheckboxAnswer(savedQuestion.getId(), List.of(savedOptionItem1.getId())); + + // when, then + assertThatCode(() -> checkBoxAnswerValidator.validate(checkboxAnswer)) + .isInstanceOf(SelectedOptionItemCountOutOfRangeException.class); + } + + @Test + void 옵션그룹에서_정한_최대_선택_수_보다_많이_선택하면_예외가_발생한다() { + // given + Question savedQuestion = questionRepository.save(선택형_필수_질문()); + OptionGroup savedOptionGroup = optionGroupRepository.save( + new OptionGroup(savedQuestion.getId(), 1, 1) + ); + OptionItem savedOptionItem1 = optionItemRepository.save(선택지(savedOptionGroup.getId(), 1)); + OptionItem savedOptionItem2 = optionItemRepository.save(선택지(savedOptionGroup.getId(), 2)); + + CheckboxAnswer checkboxAnswer = new CheckboxAnswer( + savedQuestion.getId(), List.of(savedOptionItem1.getId(), savedOptionItem2.getId())); + + // when, then + assertThatCode(() -> checkBoxAnswerValidator.validate(checkboxAnswer)) + .isInstanceOf(SelectedOptionItemCountOutOfRangeException.class); + } +} diff --git a/backend/src/test/java/reviewme/review/service/module/ReviewMapperTest.java b/backend/src/test/java/reviewme/review/service/module/ReviewMapperTest.java new file mode 100644 index 000000000..3a8d51024 --- /dev/null +++ b/backend/src/test/java/reviewme/review/service/module/ReviewMapperTest.java @@ -0,0 +1,137 @@ +package reviewme.review.service.module; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static reviewme.fixture.OptionGroupFixture.선택지_그룹; +import static reviewme.fixture.OptionItemFixture.선택지; +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.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.Review; +import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException; +import reviewme.review.service.dto.request.ReviewAnswerRequest; +import reviewme.review.service.dto.request.ReviewRegisterRequest; +import reviewme.review.service.exception.SubmittedQuestionNotFoundException; +import reviewme.reviewgroup.domain.ReviewGroup; +import reviewme.reviewgroup.repository.ReviewGroupRepository; +import reviewme.support.ServiceTest; +import reviewme.template.domain.Section; +import reviewme.template.repository.SectionRepository; +import reviewme.template.repository.TemplateRepository; + +@ServiceTest +class ReviewMapperTest { + + @Autowired + private ReviewMapper reviewMapper; + + @Autowired + private ReviewGroupRepository reviewGroupRepository; + + @Autowired + private OptionGroupRepository optionGroupRepository; + + @Autowired + private OptionItemRepository optionItemRepository; + + @Autowired + private QuestionRepository questionRepository; + + @Autowired + private SectionRepository sectionRepository; + + @Autowired + private TemplateRepository templateRepository; + + @Test + void 텍스트가_포함된_리뷰를_생성한다() { + // given + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + Question question = questionRepository.save(서술형_필수_질문()); + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); + templateRepository.save(템플릿(List.of(section.getId()))); + + String expectedTextAnswer = "답".repeat(20); + ReviewAnswerRequest reviewAnswerRequest = new ReviewAnswerRequest(question.getId(), null, expectedTextAnswer); + ReviewRegisterRequest reviewRegisterRequest = new ReviewRegisterRequest(reviewGroup.getReviewRequestCode(), List.of(reviewAnswerRequest)); + + // when + Review review = reviewMapper.mapToReview(reviewRegisterRequest); + + // then + assertThat(review.getTextAnswers()).hasSize(1); + } + + @Test + void 체크박스가_포함된_리뷰를_생성한다() { + // given + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + Question question = questionRepository.save(선택형_필수_질문()); + OptionGroup optionGroup = optionGroupRepository.save(선택지_그룹(question.getId())); + OptionItem optionItem1 = optionItemRepository.save(선택지(optionGroup.getId())); + OptionItem optionItem2 = optionItemRepository.save(선택지(optionGroup.getId())); + + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); + templateRepository.save(템플릿(List.of(section.getId()))); + + ReviewAnswerRequest reviewAnswerRequest = new ReviewAnswerRequest(question.getId(), List.of(optionItem1.getId()), null); + ReviewRegisterRequest reviewRegisterRequest = new ReviewRegisterRequest(reviewGroup.getReviewRequestCode(), List.of(reviewAnswerRequest)); + + // when + Review review = reviewMapper.mapToReview(reviewRegisterRequest); + + // then + assertThat(review.getCheckboxAnswers()).hasSize(1); + } + + @Test + void 잘못된_리뷰_요청_코드로_리뷰를_생성할_경우_예외가_발생한다() { + // given + String reviewRequestCode = "notExistCode"; + Question savedQuestion = questionRepository.save(서술형_필수_질문()); + ReviewAnswerRequest emptyTextReviewRequest = new ReviewAnswerRequest( + savedQuestion.getId(), null, ""); + ReviewRegisterRequest reviewRegisterRequest = new ReviewRegisterRequest( + reviewRequestCode, List.of(emptyTextReviewRequest)); + + // when, then + assertThatThrownBy(() -> reviewMapper.mapToReview( + reviewRegisterRequest)) + .isInstanceOf(ReviewGroupNotFoundByReviewRequestCodeException.class); + } + + @Test + void 답변에_해당하는_질문이_없는_리뷰를_생성할_경우_예외가_발생한다() { + // given + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + Question question = questionRepository.save(서술형_필수_질문()); + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); + templateRepository.save(템플릿(List.of(section.getId()))); + + long notSavedQuestionId = 100L; + ReviewAnswerRequest notQuestionAnswerRequest = new ReviewAnswerRequest( + notSavedQuestionId, null, ""); + ReviewRegisterRequest reviewRegisterRequest = new ReviewRegisterRequest( + reviewGroup.getReviewRequestCode(), List.of(notQuestionAnswerRequest)); + + // when, then + assertThatThrownBy(() -> reviewMapper.mapToReview( + reviewRegisterRequest)) + .isInstanceOf(SubmittedQuestionNotFoundException.class); + } +} diff --git a/backend/src/test/java/reviewme/review/service/ReviewPreviewGeneratorTest.java b/backend/src/test/java/reviewme/review/service/module/ReviewPreviewGeneratorTest.java similarity index 97% rename from backend/src/test/java/reviewme/review/service/ReviewPreviewGeneratorTest.java rename to backend/src/test/java/reviewme/review/service/module/ReviewPreviewGeneratorTest.java index 206fade70..d47380282 100644 --- a/backend/src/test/java/reviewme/review/service/ReviewPreviewGeneratorTest.java +++ b/backend/src/test/java/reviewme/review/service/module/ReviewPreviewGeneratorTest.java @@ -1,4 +1,4 @@ -package reviewme.review.service; +package reviewme.review.service.module; import static org.assertj.core.api.Assertions.assertThat; diff --git a/backend/src/test/java/reviewme/review/service/module/ReviewValidatorTest.java b/backend/src/test/java/reviewme/review/service/module/ReviewValidatorTest.java new file mode 100644 index 000000000..cb940128f --- /dev/null +++ b/backend/src/test/java/reviewme/review/service/module/ReviewValidatorTest.java @@ -0,0 +1,154 @@ +package reviewme.review.service.module; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +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.ReviewGroupFixture.리뷰_그룹; +import static reviewme.fixture.SectionFixture.조건부로_보이는_섹션; +import static reviewme.fixture.SectionFixture.항상_보이는_섹션; +import static reviewme.fixture.TemplateFixture.템플릿; + +import java.util.ArrayList; +import java.util.List; +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.service.exception.MissingRequiredQuestionException; +import reviewme.review.service.exception.SubmittedQuestionAndProvidedQuestionMismatchException; +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 ReviewValidatorTest { + + @Autowired + private QuestionRepository questionRepository; + + @Autowired + private OptionGroupRepository optionGroupRepository; + + @Autowired + private OptionItemRepository optionItemRepository; + + @Autowired + private ReviewGroupRepository reviewGroupRepository; + + @Autowired + private TemplateRepository templateRepository; + + @Autowired + private SectionRepository sectionRepository; + + @Autowired + private ReviewValidator reviewValidator; + + @Test + void 템플릿에_있는_질문에_대한_답과_필수_질문에_모두_응답하는_경우_예외가_발생하지_않는다() { + // 리뷰 그룹 저장 + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + // 필수가 아닌 서술형 질문 저장 + Question notRequiredTextQuestion = questionRepository.save(서술형_옵션_질문()); + Section visibleSection1 = sectionRepository.save(항상_보이는_섹션(List.of(notRequiredTextQuestion.getId()), 1)); + + // 필수 선택형 질문, 섹션 저장 + Question requiredCheckQuestion = questionRepository.save(선택형_필수_질문()); + OptionGroup requiredOptionGroup = optionGroupRepository.save(선택지_그룹(requiredCheckQuestion.getId())); + OptionItem requiredOptionItem1 = optionItemRepository.save(선택지(requiredOptionGroup.getId())); + OptionItem requiredOptionItem2 = optionItemRepository.save(선택지(requiredOptionGroup.getId())); + Section visibleSection2 = sectionRepository.save(항상_보이는_섹션(List.of(requiredCheckQuestion.getId()), 2)); + + // optionItem 선택에 따라서 required 가 달라지는 섹션1 저장 + Question conditionalTextQuestion1 = questionRepository.save(서술형_필수_질문()); + Question conditionalCheckQuestion = questionRepository.save(선택형_필수_질문()); + OptionGroup conditionalOptionGroup = optionGroupRepository.save(선택지_그룹(conditionalCheckQuestion.getId())); + OptionItem conditionalOptionItem = optionItemRepository.save(선택지(conditionalOptionGroup.getId())); + Section conditionalSection1 = sectionRepository.save(조건부로_보이는_섹션( + List.of(conditionalTextQuestion1.getId(), conditionalCheckQuestion.getId()), + requiredOptionItem1.getId(), 3) + ); + + // optionItem 선택에 따라서 required 가 달라지는 섹션2 저장 + Question conditionalQuestion2 = questionRepository.save(서술형_필수_질문()); + Section conditionalSection2 = sectionRepository.save(조건부로_보이는_섹션( + List.of(conditionalQuestion2.getId()), requiredOptionItem2.getId(), 3) + ); + + // 템플릿 저장 + Template template = templateRepository.save(템플릿( + List.of(visibleSection1.getId(), visibleSection2.getId(), + conditionalSection1.getId(), conditionalSection2.getId()) + )); + + // 각 질문에 대한 답변 생성 + TextAnswer notRequiredlTextAnswer = new TextAnswer(notRequiredTextQuestion.getId(), "답변".repeat(30)); + CheckboxAnswer alwaysRequiredCheckAnswer = new CheckboxAnswer(requiredCheckQuestion.getId(), + List.of(requiredOptionItem1.getId())); + TextAnswer conditionalTextAnswer1 = new TextAnswer(conditionalTextQuestion1.getId(), "답변".repeat(30)); + CheckboxAnswer conditionalCheckAnswer1 = new CheckboxAnswer(conditionalCheckQuestion.getId(), + List.of(conditionalOptionItem.getId())); + + // 리뷰 생성 + Review review = new Review(template.getId(), reviewGroup.getId(), + List.of(notRequiredlTextAnswer, conditionalTextAnswer1), + List.of(alwaysRequiredCheckAnswer, conditionalCheckAnswer1)); + + // when, then + assertThatCode(() -> reviewValidator.validate(review)) + .doesNotThrowAnyException(); + } + + @Test + void 제공된_템플릿에_없는_질문에_대한_답변이_있을_경우_예외가_발생한다() { + // given + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + Question question1 = questionRepository.save(서술형_필수_질문()); + Question question2 = questionRepository.save(서술형_필수_질문()); + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question1.getId()))); + Template template = templateRepository.save(템플릿(List.of(section.getId()))); + + TextAnswer textAnswer = new TextAnswer(question2.getId(), "답변".repeat(20)); + Review review = new Review(template.getId(), reviewGroup.getId(), List.of(textAnswer), new ArrayList<>()); + + // when, then + assertThatThrownBy(() -> reviewValidator.validate(review)) + .isInstanceOf(SubmittedQuestionAndProvidedQuestionMismatchException.class); + } + + @Test + void 필수_질문에_답변하지_않은_경우_예외가_발생한다() { + // given + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + + Question requiredQuestion = questionRepository.save(서술형_필수_질문()); + Question optionalQuestion = questionRepository.save(서술형_옵션_질문()); + Section section = sectionRepository.save( + 항상_보이는_섹션(List.of(requiredQuestion.getId(), optionalQuestion.getId()))); + Template template = templateRepository.save(템플릿(List.of(section.getId()))); + + TextAnswer optionalTextAnswer = new TextAnswer(optionalQuestion.getId(), "답변".repeat(20)); + Review review = new Review(template.getId(), reviewGroup.getId(), List.of(optionalTextAnswer), List.of()); + + // when, then + assertThatThrownBy(() -> reviewValidator.validate(review)) + .isInstanceOf(MissingRequiredQuestionException.class); + } +} diff --git a/backend/src/test/java/reviewme/review/service/module/TextAnswerValidatorTest.java b/backend/src/test/java/reviewme/review/service/module/TextAnswerValidatorTest.java new file mode 100644 index 000000000..6f365a37d --- /dev/null +++ b/backend/src/test/java/reviewme/review/service/module/TextAnswerValidatorTest.java @@ -0,0 +1,50 @@ +package reviewme.review.service.module; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static reviewme.fixture.QuestionFixture.서술형_필수_질문; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import reviewme.question.domain.Question; +import reviewme.question.repository.QuestionRepository; +import reviewme.review.domain.TextAnswer; +import reviewme.review.domain.exception.InvalidTextAnswerLengthException; +import reviewme.review.service.exception.SubmittedQuestionNotFoundException; +import reviewme.support.ServiceTest; + +@ServiceTest +class TextAnswerValidatorTest { + + @Autowired + private TextAnswerValidator textAnswerValidator; + + @Autowired + private QuestionRepository questionRepository; + + @Test + void 저장되지_않은_질문에_대한_대답이면_예외가_발생한다() { + // given + long notSavedQuestionId = 100L; + TextAnswer textAnswer = new TextAnswer(notSavedQuestionId, "텍스트형 응답"); + + // when, then + assertThatCode(() -> textAnswerValidator.validate(textAnswer)) + .isInstanceOf(SubmittedQuestionNotFoundException.class); + } + + @ParameterizedTest + @ValueSource(ints = {19, 10001}) + void 답변_길이가_유효하지_않으면_예외가_발생한다(int length) { + // given + String content = "답".repeat(length); + Question savedQuestion = questionRepository.save(서술형_필수_질문()); + TextAnswer textAnswer = new TextAnswer(savedQuestion.getId(), content); + + // when, then + assertThatThrownBy(() -> textAnswerValidator.validate(textAnswer)) + .isInstanceOf(InvalidTextAnswerLengthException.class); + } +} diff --git a/backend/src/test/java/reviewme/template/service/TemplateMapperTest.java b/backend/src/test/java/reviewme/template/service/TemplateMapperTest.java index 984df25df..d0bf0d6be 100644 --- a/backend/src/test/java/reviewme/template/service/TemplateMapperTest.java +++ b/backend/src/test/java/reviewme/template/service/TemplateMapperTest.java @@ -28,6 +28,7 @@ import reviewme.template.service.dto.response.QuestionResponse; import reviewme.template.service.dto.response.SectionResponse; import reviewme.template.service.dto.response.TemplateResponse; +import reviewme.template.service.module.TemplateMapper; @ServiceTest class TemplateMapperTest { @@ -77,7 +78,7 @@ class TemplateMapperTest { reviewGroupRepository.save(reviewGroup); // when - TemplateResponse templateResponse = templateMapper.mapToTemplateResponse(reviewGroup, template); + TemplateResponse templateResponse = templateMapper.mapToTemplateResponse(reviewGroup); // then assertAll( @@ -107,7 +108,7 @@ class TemplateMapperTest { reviewGroupRepository.save(reviewGroup); // when - TemplateResponse templateResponse = templateMapper.mapToTemplateResponse(reviewGroup, template); + TemplateResponse templateResponse = templateMapper.mapToTemplateResponse(reviewGroup); // then SectionResponse sectionResponse = templateResponse.sections().get(0); @@ -136,7 +137,7 @@ class TemplateMapperTest { reviewGroupRepository.save(reviewGroup); // when - TemplateResponse templateResponse = templateMapper.mapToTemplateResponse(reviewGroup, template); + TemplateResponse templateResponse = templateMapper.mapToTemplateResponse(reviewGroup); // then QuestionResponse questionResponse = templateResponse.sections().get(0).questions().get(0); @@ -162,7 +163,7 @@ class TemplateMapperTest { reviewGroupRepository.save(reviewGroup); // when - TemplateResponse templateResponse = templateMapper.mapToTemplateResponse(reviewGroup, template); + TemplateResponse templateResponse = templateMapper.mapToTemplateResponse(reviewGroup); // then QuestionResponse questionResponse = templateResponse.sections().get(0).questions().get(0); @@ -179,7 +180,7 @@ class TemplateMapperTest { reviewGroupRepository.save(reviewGroup); // when, then - assertThatThrownBy(() -> templateMapper.mapToTemplateResponse(reviewGroup, template)) + assertThatThrownBy(() -> templateMapper.mapToTemplateResponse(reviewGroup)) .isInstanceOf(SectionInTemplateNotFoundException.class); } @@ -204,7 +205,7 @@ class TemplateMapperTest { reviewGroupRepository.save(reviewGroup); // when, then - assertThatThrownBy(() -> templateMapper.mapToTemplateResponse(reviewGroup, template)) + assertThatThrownBy(() -> templateMapper.mapToTemplateResponse(reviewGroup)) .isInstanceOf(MissingOptionItemsInOptionGroupException.class); } }