diff --git a/backend/src/main/java/reviewme/question/domain/OptionGroup.java b/backend/src/main/java/reviewme/question/domain/OptionGroup.java index 4cab80b43..61aa3d23a 100644 --- a/backend/src/main/java/reviewme/question/domain/OptionGroup.java +++ b/backend/src/main/java/reviewme/question/domain/OptionGroup.java @@ -7,12 +7,14 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.AccessLevel; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Table(name = "option_group") @NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode(of = "id") @Getter public class OptionGroup { diff --git a/backend/src/main/java/reviewme/question/domain/OptionItem.java b/backend/src/main/java/reviewme/question/domain/OptionItem.java index eac0f58fd..59b29bc3b 100644 --- a/backend/src/main/java/reviewme/question/domain/OptionItem.java +++ b/backend/src/main/java/reviewme/question/domain/OptionItem.java @@ -9,12 +9,14 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.AccessLevel; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Table(name = "option_item") @NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode(of = "id") @Getter public class OptionItem { diff --git a/backend/src/main/java/reviewme/question/domain/Question2.java b/backend/src/main/java/reviewme/question/domain/Question2.java index 55ae48c2a..6639346ac 100644 --- a/backend/src/main/java/reviewme/question/domain/Question2.java +++ b/backend/src/main/java/reviewme/question/domain/Question2.java @@ -9,12 +9,14 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.AccessLevel; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Table(name = "question2") @NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode(of = "id") @Getter public class Question2 { @@ -37,4 +39,20 @@ public class Question2 { @Column(name = "position", nullable = false) private int position; + + public Question2(boolean required, QuestionType questionType, String content, String guideline, int position) { + this.required = required; + this.questionType = questionType; + this.content = content; + this.guideline = guideline; + this.position = position; + } + + public boolean isSelectable() { + return questionType == QuestionType.CHECKBOX; + } + + public boolean hasGuideline() { + return guideline != null; + } } diff --git a/backend/src/main/java/reviewme/question/domain/exception/MissingOptionGroupForQuestionException.java b/backend/src/main/java/reviewme/question/domain/exception/MissingOptionGroupForQuestionException.java new file mode 100644 index 000000000..b820ccf15 --- /dev/null +++ b/backend/src/main/java/reviewme/question/domain/exception/MissingOptionGroupForQuestionException.java @@ -0,0 +1,13 @@ +package reviewme.question.domain.exception; + +import lombok.extern.slf4j.Slf4j; +import reviewme.global.exception.NotFoundException; + +@Slf4j +public class MissingOptionGroupForQuestionException extends NotFoundException { + + public MissingOptionGroupForQuestionException(long questionId) { + super("질문에 해당하는 체크박스 그룹을 찾을 수 없어요."); + log.info("OptionGroup not found for questionId: {}", questionId); + } +} diff --git a/backend/src/main/java/reviewme/question/domain/exception/OptionItemNotFoundException.java b/backend/src/main/java/reviewme/question/domain/exception/OptionItemNotFoundException.java new file mode 100644 index 000000000..9f5200aeb --- /dev/null +++ b/backend/src/main/java/reviewme/question/domain/exception/OptionItemNotFoundException.java @@ -0,0 +1,13 @@ +package reviewme.question.domain.exception; + +import lombok.extern.slf4j.Slf4j; +import reviewme.global.exception.NotFoundException; + +@Slf4j +public class OptionItemNotFoundException extends NotFoundException { + + public OptionItemNotFoundException(long id) { + super("선택지가 존재하지 않아요."); + log.info("OptionItemNotFoundException is occurred - id: {}", id); + } +} diff --git a/backend/src/main/java/reviewme/question/repository/OptionGroupRepository.java b/backend/src/main/java/reviewme/question/repository/OptionGroupRepository.java index 8042be186..2bbc3b2c8 100644 --- a/backend/src/main/java/reviewme/question/repository/OptionGroupRepository.java +++ b/backend/src/main/java/reviewme/question/repository/OptionGroupRepository.java @@ -1,9 +1,18 @@ package reviewme.question.repository; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import reviewme.question.domain.OptionGroup; +import reviewme.question.domain.exception.MissingOptionGroupForQuestionException; @Repository public interface OptionGroupRepository extends JpaRepository { + + Optional findByQuestionId(long questionId); + + default OptionGroup getByQuestionId(long questionId) { + return findByQuestionId(questionId) + .orElseThrow(() -> new MissingOptionGroupForQuestionException(questionId)); + } } diff --git a/backend/src/main/java/reviewme/question/repository/OptionItemRepository.java b/backend/src/main/java/reviewme/question/repository/OptionItemRepository.java index a0ce20c9c..78403a045 100644 --- a/backend/src/main/java/reviewme/question/repository/OptionItemRepository.java +++ b/backend/src/main/java/reviewme/question/repository/OptionItemRepository.java @@ -1,14 +1,47 @@ package reviewme.question.repository; import java.util.List; +import java.util.Optional; +import java.util.Set; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import reviewme.question.domain.OptionItem; import reviewme.question.domain.OptionType; +import reviewme.question.domain.exception.OptionItemNotFoundException; @Repository public interface OptionItemRepository extends JpaRepository { + Optional findById(long id); + + default OptionItem getOptionItemById(long id) { + return findById(id) + .orElseThrow(() -> new OptionItemNotFoundException(id)); + } + + @Query(value = """ + SELECT o.id FROM option_item o + LEFT JOIN checkbox_answer ca + LEFT JOIN checkbox_answer_selected_option c + ON c.checkbox_answer_id = ca.id + WHERE ca.review_id = :reviewId + AND c.selected_option_id = o.id + """, nativeQuery = true) + Set findSelectedOptionItemIdsByReviewId(long reviewId); + + @Query(value = """ + SELECT o.* FROM option_item o + LEFT JOIN checkbox_answer ca + LEFT JOIN checkbox_answer_selected_option c + ON c.checkbox_answer_id = ca.id + WHERE ca.review_id = :reviewId + AND ca.question_id = :questionId + AND c.selected_option_id = o.id + ORDER BY o.position ASC + """, nativeQuery = true) + List findSelectedOptionItemsByReviewIdAndQuestionId(long reviewId, long questionId); + List findAllByOptionType(OptionType optionType); boolean existsByOptionTypeAndId(OptionType optionType, long id); diff --git a/backend/src/main/java/reviewme/review/controller/ReviewController.java b/backend/src/main/java/reviewme/review/controller/ReviewController.java index 0e6561c09..b282f0878 100644 --- a/backend/src/main/java/reviewme/review/controller/ReviewController.java +++ b/backend/src/main/java/reviewme/review/controller/ReviewController.java @@ -16,7 +16,9 @@ import reviewme.review.dto.response.ReceivedReviewsResponse2; import reviewme.review.dto.response.ReviewDetailResponse; import reviewme.review.dto.response.ReviewSetupResponse; +import reviewme.review.service.ReviewDetailLookupService; import reviewme.review.service.ReviewService; +import reviewme.review.service.dto.response.detail.TemplateAnswerResponse; @RestController @RequiredArgsConstructor @@ -25,6 +27,7 @@ public class ReviewController implements ReviewApi { private static final String GROUP_ACCESS_CODE_HEADER = "GroupAccessCode"; private final ReviewService reviewService; + private final ReviewDetailLookupService reviewDetailLookupService; @PostMapping("/reviews") public ResponseEntity createReview(@Valid @RequestBody CreateReviewRequest request) { @@ -61,4 +64,12 @@ public ResponseEntity findReceivedReviewDetail( ReviewDetailResponse response = reviewService.findReceivedReviewDetail(groupAccessCode, id); return ResponseEntity.ok(response); } + + @GetMapping("/v2/reviews/{id}") + public ResponseEntity findReceivedReviewDetailV2( + @PathVariable long id, + @HeaderProperty(GROUP_ACCESS_CODE_HEADER) String groupAccessCode) { + TemplateAnswerResponse response = reviewDetailLookupService.getReviewDetail(groupAccessCode, id); + return ResponseEntity.ok(response); + } } diff --git a/backend/src/main/java/reviewme/review/domain/CheckBoxAnswerSelectedOption.java b/backend/src/main/java/reviewme/review/domain/CheckBoxAnswerSelectedOption.java new file mode 100644 index 000000000..8a19dc049 --- /dev/null +++ b/backend/src/main/java/reviewme/review/domain/CheckBoxAnswerSelectedOption.java @@ -0,0 +1,32 @@ +package reviewme.review.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "checkbox_answer_selected_option") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class CheckBoxAnswerSelectedOption { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "checkbox_answer_id", nullable = false, insertable = false, updatable = false) + private long checkboxAnswerId; + + @Column(name = "selected_option_id", nullable = false) + private long selectedOptionId; + + public CheckBoxAnswerSelectedOption(long selectedOptionId) { + this.selectedOptionId = selectedOptionId; + } +} diff --git a/backend/src/main/java/reviewme/review/domain/CheckboxAnswer.java b/backend/src/main/java/reviewme/review/domain/CheckboxAnswer.java index 60067fa68..6f6cf5aab 100644 --- a/backend/src/main/java/reviewme/review/domain/CheckboxAnswer.java +++ b/backend/src/main/java/reviewme/review/domain/CheckboxAnswer.java @@ -1,21 +1,25 @@ package reviewme.review.domain; -import jakarta.persistence.CollectionTable; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; -import jakarta.persistence.ElementCollection; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import java.util.List; import lombok.AccessLevel; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Table(name = "checkbox_answer") @NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode(of = "id") @Getter public class CheckboxAnswer { @@ -23,15 +27,20 @@ public class CheckboxAnswer { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(name = "review_id", nullable = false, insertable = false, updatable = false) + private long reviewId; + @Column(name = "question_id", nullable = false) private long questionId; - @ElementCollection - @CollectionTable(name = "selected_option_ids") - private List selectedOptionIds; + @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "checkbox_answer_id", nullable = false, updatable = false) + private List selectedOptionIds; public CheckboxAnswer(long questionId, List selectedOptionIds) { this.questionId = questionId; - this.selectedOptionIds = selectedOptionIds; + this.selectedOptionIds = selectedOptionIds.stream() + .map(CheckBoxAnswerSelectedOption::new) + .toList(); } } diff --git a/backend/src/main/java/reviewme/review/domain/Review2.java b/backend/src/main/java/reviewme/review/domain/Review2.java index d860a89d4..251b54eda 100644 --- a/backend/src/main/java/reviewme/review/domain/Review2.java +++ b/backend/src/main/java/reviewme/review/domain/Review2.java @@ -3,21 +3,25 @@ import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import lombok.AccessLevel; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Table(name = "review2") @NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode(of = "id") @Getter public class Review2 { @@ -31,11 +35,11 @@ public class Review2 { @Column(name = "review_group_id", nullable = false) private long reviewGroupId; - @OneToMany(cascade = CascadeType.PERSIST) + @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) @JoinColumn(name = "review_id", nullable = false, updatable = false) private List textAnswers; - @OneToMany(cascade = CascadeType.PERSIST) + @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) @JoinColumn(name = "review_id", nullable = false, updatable = false) private List checkboxAnswers; @@ -43,12 +47,15 @@ public class Review2 { private LocalDateTime createdAt; public Review2(long templateId, long reviewGroupId, - List textAnswers, List checkboxAnswers, - LocalDateTime createdAt) { + List textAnswers, List checkboxAnswers) { this.templateId = templateId; this.reviewGroupId = reviewGroupId; this.textAnswers = textAnswers; this.checkboxAnswers = checkboxAnswers; - this.createdAt = createdAt; + this.createdAt = LocalDateTime.now(); + } + + 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 d356f1900..5200d6ff9 100644 --- a/backend/src/main/java/reviewme/review/domain/TextAnswer.java +++ b/backend/src/main/java/reviewme/review/domain/TextAnswer.java @@ -7,12 +7,14 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.AccessLevel; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Table(name = "text_answer") @NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode(of = "id") @Getter public class TextAnswer { @@ -23,11 +25,11 @@ public class TextAnswer { @Column(name = "question_id", nullable = false) private long questionId; - @Column(name = "text", nullable = false, length = 1_000) - private String text; + @Column(name = "content", nullable = false, length = 1_000) + private String content; - public TextAnswer(long questionId, String text) { + public TextAnswer(long questionId, String content) { this.questionId = questionId; - this.text = text; + this.content = content; } } diff --git a/backend/src/main/java/reviewme/review/domain/TextAnswers.java b/backend/src/main/java/reviewme/review/domain/TextAnswers.java new file mode 100644 index 000000000..3ef84e1f1 --- /dev/null +++ b/backend/src/main/java/reviewme/review/domain/TextAnswers.java @@ -0,0 +1,26 @@ +package reviewme.review.domain; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import reviewme.review.domain.exception.MissingTextAnswerForQuestionException; + +@Slf4j +public class TextAnswers { + + private final Map textAnswers; + + public TextAnswers(List textAnswers) { + this.textAnswers = textAnswers.stream() + .collect(Collectors.toMap(TextAnswer::getQuestionId, Function.identity())); + } + + public TextAnswer getAnswerByQuestionId(long questionId) { + if (!textAnswers.containsKey(questionId)) { + throw new MissingTextAnswerForQuestionException(questionId); + } + return textAnswers.get(questionId); + } +} diff --git a/backend/src/main/java/reviewme/review/domain/exception/ReviewIsNotInReviewGroupException.java b/backend/src/main/java/reviewme/review/domain/exception/InvalidReviewAccessByReviewGroupException.java similarity index 52% rename from backend/src/main/java/reviewme/review/domain/exception/ReviewIsNotInReviewGroupException.java rename to backend/src/main/java/reviewme/review/domain/exception/InvalidReviewAccessByReviewGroupException.java index 72442d522..6716a8348 100644 --- a/backend/src/main/java/reviewme/review/domain/exception/ReviewIsNotInReviewGroupException.java +++ b/backend/src/main/java/reviewme/review/domain/exception/InvalidReviewAccessByReviewGroupException.java @@ -4,10 +4,10 @@ import reviewme.global.exception.BadRequestException; @Slf4j -public class ReviewIsNotInReviewGroupException extends BadRequestException { +public class InvalidReviewAccessByReviewGroupException extends BadRequestException { - public ReviewIsNotInReviewGroupException(long reviewId, long reviewGroupId) { - super("리뷰 그룹에 해당하는 리뷰가 아니에요."); + public InvalidReviewAccessByReviewGroupException(long reviewId, long reviewGroupId) { + super("리뷰가 존재하지 않아요."); log.info("Review is not in review group - reviewId: {}, reviewGroupId: {}", reviewId, reviewGroupId); } } diff --git a/backend/src/main/java/reviewme/review/domain/exception/MissingTextAnswerForQuestionException.java b/backend/src/main/java/reviewme/review/domain/exception/MissingTextAnswerForQuestionException.java new file mode 100644 index 000000000..09550f007 --- /dev/null +++ b/backend/src/main/java/reviewme/review/domain/exception/MissingTextAnswerForQuestionException.java @@ -0,0 +1,13 @@ +package reviewme.review.domain.exception; + +import lombok.extern.slf4j.Slf4j; +import reviewme.global.exception.NotFoundException; + +@Slf4j +public class MissingTextAnswerForQuestionException extends NotFoundException { + + public MissingTextAnswerForQuestionException(long questionId) { + super("질문에 해당하는 서술형 답변을 찾지 못했어요."); + log.warn("Text Answer not found for questionId: {}", questionId); + } +} diff --git a/backend/src/main/java/reviewme/review/repository/QuestionRepository2.java b/backend/src/main/java/reviewme/review/repository/QuestionRepository2.java new file mode 100644 index 000000000..bf3765ddc --- /dev/null +++ b/backend/src/main/java/reviewme/review/repository/QuestionRepository2.java @@ -0,0 +1,18 @@ +package reviewme.review.repository; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import reviewme.question.domain.Question2; + +public interface QuestionRepository2 extends JpaRepository { + + @Query(value = """ + SELECT q.* FROM question2 q + LEFT JOIN section_question sq + ON sq.question_id = q.id + WHERE sq.section_id = :sectionId + ORDER BY q.position ASC + """, nativeQuery = true) + List findAllBySectionId(long sectionId); +} diff --git a/backend/src/main/java/reviewme/review/repository/ReviewRepository2.java b/backend/src/main/java/reviewme/review/repository/ReviewRepository2.java new file mode 100644 index 000000000..2553956b2 --- /dev/null +++ b/backend/src/main/java/reviewme/review/repository/ReviewRepository2.java @@ -0,0 +1,10 @@ +package reviewme.review.repository; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import reviewme.review.domain.Review2; + +public interface ReviewRepository2 extends JpaRepository { + + Optional findByIdAndReviewGroupId(long reviewId, long reviewGroupId); +} diff --git a/backend/src/main/java/reviewme/review/service/ReviewDetailLookupService.java b/backend/src/main/java/reviewme/review/service/ReviewDetailLookupService.java new file mode 100644 index 000000000..3651a1de7 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/ReviewDetailLookupService.java @@ -0,0 +1,129 @@ +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.Question2; +import reviewme.question.repository.OptionGroupRepository; +import reviewme.question.repository.OptionItemRepository; +import reviewme.review.domain.Review2; +import reviewme.review.domain.TextAnswer; +import reviewme.review.domain.TextAnswers; +import reviewme.review.domain.exception.ReviewGroupNotFoundByGroupAccessCodeException; +import reviewme.review.domain.exception.InvalidReviewAccessByReviewGroupException; +import reviewme.review.repository.QuestionRepository2; +import reviewme.review.repository.ReviewRepository2; +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.reviewgroup.domain.ReviewGroup; +import reviewme.reviewgroup.repository.ReviewGroupRepository; +import reviewme.template.domain.Section; +import reviewme.template.repository.SectionRepository; + +@Service +@Transactional(readOnly = true) +@AllArgsConstructor +public class ReviewDetailLookupService { + + private final SectionRepository sectionRepository; + private final ReviewRepository2 reviewRepository; + private final ReviewGroupRepository reviewGroupRepository; + private final QuestionRepository2 questionRepository; + private final OptionItemRepository optionItemRepository; + private final OptionGroupRepository optionGroupRepository; + + public TemplateAnswerResponse getReviewDetail(String groupAccessCode, long reviewId) { + ReviewGroup reviewGroup = reviewGroupRepository.findByGroupAccessCode(groupAccessCode) + .orElseThrow(() -> new ReviewGroupNotFoundByGroupAccessCodeException(groupAccessCode)); + + Review2 review = reviewRepository.findByIdAndReviewGroupId(reviewId, reviewGroup.getId()) + .orElseThrow(() -> new InvalidReviewAccessByReviewGroupException(reviewId, reviewGroup.getId())); + long templateId = review.getTemplateId(); + + Set selectedOptionItemIds = optionItemRepository.findSelectedOptionItemIdsByReviewId(reviewId); + List sectionResponses = sectionRepository.findAllByTemplateId(templateId) + .stream() + .filter(section -> section.isVisibleBySelectedOptionIds(selectedOptionItemIds)) + .map(section -> getSectionAnswerResponse(review, section)) + .toList(); + + return new TemplateAnswerResponse( + templateId, + reviewGroup.getReviewee(), + reviewGroup.getProjectName(), + review.getCreatedDate(), + sectionResponses + ); + } + + private SectionAnswerResponse getSectionAnswerResponse(Review2 review, Section section) { + TextAnswers textAnswers = new TextAnswers(review.getTextAnswers()); + ArrayList questionResponses = new ArrayList<>(); + + for (Question2 question : questionRepository.findAllBySectionId(section.getId())) { + if (question.isSelectable()) { + questionResponses.add(getCheckboxAnswerResponse(review, question)); + continue; + } + questionResponses.add(getTextAnswerResponse(question, textAnswers)); + } + + return new SectionAnswerResponse( + section.getId(), + section.getHeader(), + questionResponses + ); + } + + private QuestionAnswerResponse getTextAnswerResponse(Question2 question, TextAnswers textAnswers) { + TextAnswer textAnswer = textAnswers.getAnswerByQuestionId(question.getId()); + return new QuestionAnswerResponse( + question.getId(), + question.isRequired(), + question.getQuestionType(), + question.getContent(), + question.hasGuideline(), + question.getGuideline(), + null, + textAnswer.getContent() + ); + } + + private QuestionAnswerResponse getCheckboxAnswerResponse(Review2 review, Question2 question) { + OptionGroup optionGroup = optionGroupRepository.getByQuestionId(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.getContent(), + question.hasGuideline(), + question.getGuideline(), + optionGroupAnswerResponse, + null + ); + } +} diff --git a/backend/src/main/java/reviewme/review/service/ReviewPreviewGenerator.java b/backend/src/main/java/reviewme/review/service/ReviewPreviewGenerator.java index 1f434fe03..b83cacf5f 100644 --- a/backend/src/main/java/reviewme/review/service/ReviewPreviewGenerator.java +++ b/backend/src/main/java/reviewme/review/service/ReviewPreviewGenerator.java @@ -23,7 +23,7 @@ public String generatePreview2(List reviewTextAnswers) { if (reviewTextAnswers.isEmpty()) { return ""; } - String answer = reviewTextAnswers.get(0).getText(); + String answer = reviewTextAnswers.get(0).getContent(); if (answer.length() > PREVIEW_LENGTH) { return answer.substring(0, PREVIEW_LENGTH); } diff --git a/backend/src/main/java/reviewme/review/service/ReviewService.java b/backend/src/main/java/reviewme/review/service/ReviewService.java index e32ebad4c..c333fec55 100644 --- a/backend/src/main/java/reviewme/review/service/ReviewService.java +++ b/backend/src/main/java/reviewme/review/service/ReviewService.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import reviewme.keyword.repository.KeywordRepository; +import reviewme.question.domain.OptionItem; import reviewme.question.domain.OptionType; import reviewme.question.domain.Question; import reviewme.question.repository.OptionItemRepository; @@ -15,9 +16,9 @@ import reviewme.review.domain.ReviewContent; import reviewme.review.domain.ReviewKeyword; import reviewme.review.domain.exception.CategoryOptionByReviewNotFoundException; +import reviewme.review.domain.exception.InvalidReviewAccessByReviewGroupException; import reviewme.review.domain.exception.ReviewGroupNotFoundByGroupAccessCodeException; import reviewme.review.domain.exception.ReviewGroupNotFoundByRequestReviewCodeException; -import reviewme.review.domain.exception.ReviewIsNotInReviewGroupException; import reviewme.review.dto.request.CreateReviewContentRequest; import reviewme.review.dto.request.CreateReviewRequest; import reviewme.review.dto.response.KeywordResponse; @@ -121,7 +122,7 @@ public ReviewDetailResponse findReceivedReviewDetail(String groupAccessCode, lon .orElseThrow(() -> new ReviewGroupNotFoundByGroupAccessCodeException(groupAccessCode)); Review review = reviewRepository.findByIdAndReviewGroupId(reviewId, reviewGroup.getId()) - .orElseThrow(() -> new ReviewIsNotInReviewGroupException(reviewId, reviewGroup.getId())); + .orElseThrow(() -> new InvalidReviewAccessByReviewGroupException(reviewId, reviewGroup.getId())); return createReviewDetailResponse(review, reviewGroup); } @@ -172,17 +173,22 @@ private ReceivedReviewResponse2 createReceivedReviewResponse2(Review2 review) { CheckboxAnswer checkboxAnswer = review.getCheckboxAnswers() .stream() .filter(answer -> optionItemRepository.existsByOptionTypeAndId( - OptionType.CATEGORY, answer.getSelectedOptionIds().get(0) + OptionType.CATEGORY, answer.getSelectedOptionIds().get(0).getSelectedOptionId() )) .findFirst() .orElseThrow(() -> new CategoryOptionByReviewNotFoundException(review.getId())); List categoryResponses = - optionItemRepository.findAllById(checkboxAnswer.getSelectedOptionIds()) + checkboxAnswer.getSelectedOptionIds() .stream() - .map(optionItem -> new ReceivedReviewCategoryResponse( - optionItem.getId(), optionItem.getContent() - )) + .map(checkBoxAnswerSelectedOptionId -> { + OptionItem optionItem = optionItemRepository.getOptionItemById( + checkBoxAnswerSelectedOptionId.getSelectedOptionId() + ); + return new ReceivedReviewCategoryResponse( + optionItem.getId(), optionItem.getContent() + ); + }) .toList(); return new ReceivedReviewResponse2( diff --git a/backend/src/main/java/reviewme/review/service/dto/response/detail/OptionGroupAnswerResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/detail/OptionGroupAnswerResponse.java new file mode 100644 index 000000000..894dbaae8 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/dto/response/detail/OptionGroupAnswerResponse.java @@ -0,0 +1,11 @@ +package reviewme.review.service.dto.response.detail; + +import java.util.List; + +public record OptionGroupAnswerResponse( + long optionGroupId, + long minCount, + long maxCount, + List options +) { +} diff --git a/backend/src/main/java/reviewme/review/service/dto/response/detail/OptionItemAnswerResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/detail/OptionItemAnswerResponse.java new file mode 100644 index 000000000..6bd424f5f --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/dto/response/detail/OptionItemAnswerResponse.java @@ -0,0 +1,8 @@ +package reviewme.review.service.dto.response.detail; + +public record OptionItemAnswerResponse( + long optionId, + String content, + boolean isChecked +) { +} diff --git a/backend/src/main/java/reviewme/review/service/dto/response/detail/QuestionAnswerResponse.java b/backend/src/main/java/reviewme/review/service/dto/response/detail/QuestionAnswerResponse.java new file mode 100644 index 000000000..48672e414 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/dto/response/detail/QuestionAnswerResponse.java @@ -0,0 +1,16 @@ +package reviewme.review.service.dto.response.detail; + +import jakarta.annotation.Nullable; +import reviewme.question.domain.QuestionType; + +public record QuestionAnswerResponse( + long questionId, + boolean required, + QuestionType questionType, + String content, + boolean hasGuideline, + @Nullable String guideline, + @Nullable OptionGroupAnswerResponse optionGroup, + @Nullable String answer +) { +} 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 new file mode 100644 index 000000000..ad2887644 --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/dto/response/detail/SectionAnswerResponse.java @@ -0,0 +1,10 @@ +package reviewme.review.service.dto.response.detail; + +import java.util.List; + +public record SectionAnswerResponse( + long sectionId, + String header, + List questions +) { +} 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/TemplateAnswerResponse.java new file mode 100644 index 000000000..0e838236b --- /dev/null +++ b/backend/src/main/java/reviewme/review/service/dto/response/detail/TemplateAnswerResponse.java @@ -0,0 +1,13 @@ +package reviewme.review.service.dto.response.detail; + +import java.time.LocalDate; +import java.util.List; + +public record TemplateAnswerResponse( + long formId, + String revieweeName, + String projectName, + LocalDate createdAt, + List sections +) { +} diff --git a/backend/src/main/java/reviewme/template/domain/Section.java b/backend/src/main/java/reviewme/template/domain/Section.java index 14eafd910..86e40c752 100644 --- a/backend/src/main/java/reviewme/template/domain/Section.java +++ b/backend/src/main/java/reviewme/template/domain/Section.java @@ -1,24 +1,28 @@ package reviewme.template.domain; -import jakarta.persistence.CollectionTable; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; -import jakarta.persistence.ElementCollection; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import java.util.Collection; import java.util.List; import lombok.AccessLevel; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Table(name = "section") @NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode(of = "id") @Getter public class Section { @@ -30,9 +34,9 @@ public class Section { @Enumerated(EnumType.STRING) private VisibleType visibleType; - @ElementCollection - @CollectionTable(name = "question_ids", joinColumns = @JoinColumn(name = "section_id")) - private List questionIds; + @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "section_id", nullable = false, updatable = false) + private List questionIds; @Column(name = "on_selected_option_id", nullable = true) private Long onSelectedOptionId; @@ -46,9 +50,15 @@ public class Section { public Section(VisibleType visibleType, List questionIds, Long onSelectedOptionId, String header, int position) { this.visibleType = visibleType; - this.questionIds = questionIds; + this.questionIds = questionIds.stream() + .map(SectionQuestion::new) + .toList(); this.onSelectedOptionId = onSelectedOptionId; this.header = header; this.position = position; } + + public boolean isVisibleBySelectedOptionIds(Collection selectedOptionIds) { + return visibleType == VisibleType.ALWAYS || selectedOptionIds.contains(onSelectedOptionId); + } } diff --git a/backend/src/main/java/reviewme/template/domain/SectionQuestion.java b/backend/src/main/java/reviewme/template/domain/SectionQuestion.java new file mode 100644 index 000000000..eaac6e73e --- /dev/null +++ b/backend/src/main/java/reviewme/template/domain/SectionQuestion.java @@ -0,0 +1,32 @@ +package reviewme.template.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "section_question") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class SectionQuestion { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "section_id", nullable = false, insertable = false, updatable = false) + private long sectionId; + + @Column(name = "question_id", nullable = false) + private long questionId; + + public SectionQuestion(long questionId) { + this.questionId = questionId; + } +} diff --git a/backend/src/main/java/reviewme/template/domain/Template.java b/backend/src/main/java/reviewme/template/domain/Template.java index 3a8c733b1..29b6f36d7 100644 --- a/backend/src/main/java/reviewme/template/domain/Template.java +++ b/backend/src/main/java/reviewme/template/domain/Template.java @@ -1,21 +1,24 @@ package reviewme.template.domain; -import jakarta.persistence.CollectionTable; -import jakarta.persistence.ElementCollection; +import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import java.util.List; import lombok.AccessLevel; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Table(name = "template") @NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode(of = "id") @Getter public class Template { @@ -23,11 +26,13 @@ public class Template { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ElementCollection - @CollectionTable(name = "section_ids", joinColumns = @JoinColumn(name = "template_id")) - List sectionIds; + @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "template_id", nullable = false, updatable = false) + private List sectionIds; public Template(List sectionIds) { - this.sectionIds = sectionIds; + this.sectionIds = sectionIds.stream() + .map(TemplateSection::new) + .toList(); } } diff --git a/backend/src/main/java/reviewme/template/domain/TemplateSection.java b/backend/src/main/java/reviewme/template/domain/TemplateSection.java new file mode 100644 index 000000000..6d451ee80 --- /dev/null +++ b/backend/src/main/java/reviewme/template/domain/TemplateSection.java @@ -0,0 +1,32 @@ +package reviewme.template.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "template_section") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class TemplateSection { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "template_id", nullable = false, insertable = false, updatable = false) + private long templateId; + + @Column(name = "section_id", nullable = false) + private long sectionId; + + public TemplateSection(long sectionId) { + this.sectionId = sectionId; + } +} diff --git a/backend/src/main/java/reviewme/template/repository/SectionRepository.java b/backend/src/main/java/reviewme/template/repository/SectionRepository.java index 6d03990d1..4479a8472 100644 --- a/backend/src/main/java/reviewme/template/repository/SectionRepository.java +++ b/backend/src/main/java/reviewme/template/repository/SectionRepository.java @@ -1,9 +1,19 @@ package reviewme.template.repository; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import reviewme.template.domain.Section; @Repository public interface SectionRepository extends JpaRepository { + + @Query(value = """ + SELECT s.* FROM section s LEFT JOIN template_section ts + ON ts.section_id = s.id + WHERE ts.template_id = :templateId + ORDER BY s.position ASC + """, nativeQuery = true) + List
findAllByTemplateId(long templateId); } diff --git a/backend/src/test/java/reviewme/question/repository/OptionItemRepositoryTest.java b/backend/src/test/java/reviewme/question/repository/OptionItemRepositoryTest.java new file mode 100644 index 000000000..4ed3120b6 --- /dev/null +++ b/backend/src/test/java/reviewme/question/repository/OptionItemRepositoryTest.java @@ -0,0 +1,80 @@ +package reviewme.question.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import reviewme.question.domain.OptionItem; +import reviewme.question.domain.OptionType; +import reviewme.question.domain.Question2; +import reviewme.question.domain.QuestionType; +import reviewme.review.domain.CheckboxAnswer; +import reviewme.review.domain.Review2; +import reviewme.review.repository.QuestionRepository2; +import reviewme.review.repository.ReviewRepository2; + +@DataJpaTest +class OptionItemRepositoryTest { + + @Autowired + private OptionItemRepository optionItemRepository; + + @Autowired + private ReviewRepository2 reviewRepository; + + @Autowired + private QuestionRepository2 questionRepository; + + @Test + void 리뷰_아이디로_선택한_옵션_아이템_아이디를_불러온다() { + // given + long optionId1 = optionItemRepository.save(new OptionItem("1", 0, 1, OptionType.KEYWORD)).getId(); + long optionId2 = optionItemRepository.save(new OptionItem("2", 0, 1, OptionType.KEYWORD)).getId(); + long optionId3 = optionItemRepository.save(new OptionItem("3", 0, 1, OptionType.KEYWORD)).getId(); + long optionId4 = optionItemRepository.save(new OptionItem("4", 0, 1, OptionType.KEYWORD)).getId(); + optionItemRepository.save(new OptionItem("5", 0, 1, OptionType.KEYWORD)); + + List checkboxAnswers = List.of( + new CheckboxAnswer(1, List.of(optionId1, optionId2)), + new CheckboxAnswer(2, List.of(optionId3, optionId4)) + ); + Review2 review = reviewRepository.save(new Review2(0, 0, List.of(), checkboxAnswers)); + + // when + Set actual = optionItemRepository.findSelectedOptionItemIdsByReviewId(review.getId()); + + // then + assertThat(actual).containsExactlyInAnyOrder(optionId1, optionId2, optionId3, optionId4); + } + + @Test + void 리뷰_아이디와_질문_아이디로_선택한_옵션_아이템을_순서대로_불러온다() { + // given + long optionId1 = optionItemRepository.save(new OptionItem("1", 0, 3, OptionType.KEYWORD)).getId(); + long optionId2 = optionItemRepository.save(new OptionItem("2", 0, 2, OptionType.KEYWORD)).getId(); + long optionId3 = optionItemRepository.save(new OptionItem("3", 0, 1, OptionType.KEYWORD)).getId(); + long optionId4 = optionItemRepository.save(new OptionItem("4", 0, 1, OptionType.KEYWORD)).getId(); + optionItemRepository.save(new OptionItem("5", 0, 1, OptionType.KEYWORD)); + + List checkboxAnswers = List.of( + new CheckboxAnswer(1, List.of(optionId1, optionId3)), + new CheckboxAnswer(2, List.of(optionId4)) + ); + Question2 question1 = questionRepository.save(new Question2(true, QuestionType.CHECKBOX, "질문", null, 1)); + questionRepository.save(new Question2(true, QuestionType.CHECKBOX, "질문", null, 1)); + + Review2 review = reviewRepository.save(new Review2(0, 0, List.of(), checkboxAnswers)); + + // when + List actual = optionItemRepository.findSelectedOptionItemsByReviewIdAndQuestionId( + review.getId(), question1.getId() + ); + + // then + assertThat(actual).extracting(OptionItem::getId).containsExactly(optionId3, optionId1); + } + +} diff --git a/backend/src/test/java/reviewme/review/domain/TextAnswersTest.java b/backend/src/test/java/reviewme/review/domain/TextAnswersTest.java new file mode 100644 index 000000000..34a4b1ba7 --- /dev/null +++ b/backend/src/test/java/reviewme/review/domain/TextAnswersTest.java @@ -0,0 +1,33 @@ +package reviewme.review.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.Test; +import reviewme.review.domain.exception.MissingTextAnswerForQuestionException; + +class TextAnswersTest { + + @Test + void 질문에_해당하는_답변이_없으면_예외를_발생한다() { + // given + TextAnswers textAnswers = new TextAnswers(List.of(new TextAnswer(1, "답변"))); + + // when, then + assertThatThrownBy(() -> textAnswers.getAnswerByQuestionId(2)) + .isInstanceOf(MissingTextAnswerForQuestionException.class); + } + + @Test + void 질문_ID로_서술형_답변을_반환한다() { + // given + TextAnswers textAnswers = new TextAnswers(List.of(new TextAnswer(1, "답변"))); + + // when + TextAnswer actual = textAnswers.getAnswerByQuestionId(1); + + // then + assertThat(actual.getContent()).isEqualTo("답변"); + } +} diff --git a/backend/src/test/java/reviewme/review/repository/QuestionRepository2Test.java b/backend/src/test/java/reviewme/review/repository/QuestionRepository2Test.java new file mode 100644 index 000000000..d84c01de0 --- /dev/null +++ b/backend/src/test/java/reviewme/review/repository/QuestionRepository2Test.java @@ -0,0 +1,42 @@ +package reviewme.review.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import reviewme.question.domain.Question2; +import reviewme.question.domain.QuestionType; +import reviewme.template.domain.Section; +import reviewme.template.domain.VisibleType; +import reviewme.template.repository.SectionRepository; + +@DataJpaTest +class QuestionRepository2Test { + + @Autowired + private QuestionRepository2 questionRepository; + + @Autowired + private SectionRepository sectionRepository; + + @Test + void 섹션_아이디로_질문_목록을_순서대로_가져온다() { + // given + Question2 question1 = questionRepository.save(new Question2(true, QuestionType.TEXT, "질문1", null, 1)); + Question2 question2 = questionRepository.save(new Question2(true, QuestionType.TEXT, "질문2", null, 2)); + Question2 question3 = questionRepository.save(new Question2(true, QuestionType.TEXT, "질문3", null, 3)); + questionRepository.save(new Question2(true, QuestionType.TEXT, "질문4", null, 1)); + + List questionIds = List.of(question3.getId(), question1.getId(), question2.getId()); + Section section = sectionRepository.save(new Section(VisibleType.ALWAYS, questionIds, null, "header", 0)); + + // when + List actual = questionRepository.findAllBySectionId(section.getId()); + + // then + assertThat(actual).extracting(Question2::getId) + .containsExactly(question1.getId(), question2.getId(), question3.getId()); + } +} diff --git a/backend/src/test/java/reviewme/review/repository/ReviewRepositoryTest.java b/backend/src/test/java/reviewme/review/repository/ReviewRepositoryTest.java index 7b8adda28..517707082 100644 --- a/backend/src/test/java/reviewme/review/repository/ReviewRepositoryTest.java +++ b/backend/src/test/java/reviewme/review/repository/ReviewRepositoryTest.java @@ -28,6 +28,6 @@ class ReviewRepositoryTest { // then assertThat(actual).map(Review::getId) - .containsExactly(review4.getId(), review2.getId(), review1.getId(), review3.getId()); + .containsExactly(review4.getId(), review2.getId(), review1.getId(), review3.getId()); } } diff --git a/backend/src/test/java/reviewme/review/service/ReviewDetailLookupServiceTest.java b/backend/src/test/java/reviewme/review/service/ReviewDetailLookupServiceTest.java new file mode 100644 index 000000000..d9defe07e --- /dev/null +++ b/backend/src/test/java/reviewme/review/service/ReviewDetailLookupServiceTest.java @@ -0,0 +1,126 @@ +package reviewme.review.service; + +import static org.assertj.core.api.Assertions.assertThat; + +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.OptionType; +import reviewme.question.domain.Question2; +import reviewme.question.domain.QuestionType; +import reviewme.question.repository.OptionGroupRepository; +import reviewme.question.repository.OptionItemRepository; +import reviewme.review.domain.CheckboxAnswer; +import reviewme.review.domain.Review2; +import reviewme.review.domain.TextAnswer; +import reviewme.review.repository.QuestionRepository2; +import reviewme.review.repository.ReviewRepository2; +import reviewme.review.service.dto.response.detail.TemplateAnswerResponse; +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.domain.VisibleType; +import reviewme.template.repository.SectionRepository; +import reviewme.template.repository.TemplateRepository; + +@ServiceTest +class ReviewDetailLookupServiceTest { + + @Autowired + private ReviewDetailLookupService reviewDetailLookupService; + + @Autowired + private ReviewGroupRepository reviewGroupRepository; + + @Autowired + private ReviewRepository2 reviewRepository2; + + @Autowired + private SectionRepository sectionRepository; + + @Autowired + private QuestionRepository2 questionRepository; + + @Autowired + private OptionGroupRepository optionGroupRepository; + + @Autowired + private OptionItemRepository optionItemRepository; + + @Autowired + private TemplateRepository templateRepository; + + @Test + void 사용자가_작성한_리뷰를_확인한다() { + // given + ReviewGroup reviewGroup = reviewGroupRepository.save(new ReviewGroup("aru", "reviewme", "ABCD", "0000")); + Question2 question1 = questionRepository.save(new Question2(true, QuestionType.TEXT, "질문", null, 1)); + Question2 question2 = questionRepository.save(new Question2(true, QuestionType.CHECKBOX, "질문", null, 1)); + Question2 question3 = questionRepository.save(new Question2(true, QuestionType.TEXT, "체크 1 조건", "가이드라인", 1)); + OptionGroup optionGroup = optionGroupRepository.save(new OptionGroup(question2.getId(), 1, 3)); + OptionItem optionItem1 = optionItemRepository.save(new OptionItem("체크 1", optionGroup.getId(), 1, OptionType.KEYWORD)); + OptionItem optionItem2 = optionItemRepository.save(new OptionItem("체크 2", optionGroup.getId(), 1, OptionType.KEYWORD)); + + Section section1 = sectionRepository.save( + new Section(VisibleType.ALWAYS, List.of(question1.getId(), question2.getId()), null, "1번 섹션", 1) + ); + Section section2 = sectionRepository.save( + new Section(VisibleType.CONDITIONAL, List.of(question3.getId()), optionItem1.getId(), "2번 섹션", 2) + ); + Template template = templateRepository.save(new Template(List.of(section1.getId(), section2.getId()))); + + List textAnswers = List.of( + new TextAnswer(1, "질문 1 답변"), + new TextAnswer(3, "질문 3 답변") + ); + List checkboxAnswers = List.of( + new CheckboxAnswer(2, List.of(optionItem1.getId(), optionItem2.getId())) + ); + Review2 review = reviewRepository2.save( + new Review2(template.getId(), reviewGroup.getId(), textAnswers, checkboxAnswers) + ); + + // when + TemplateAnswerResponse reviewDetail = reviewDetailLookupService.getReviewDetail("0000", review.getId()); + + // then + assertThat(reviewDetail.sections()).hasSize(2); + } + + @Test + void 섹션을_보이게_하는_옵션을_선택하지_않은_경우_해당_섹션을_제외하고_보여준다() { + // given + ReviewGroup reviewGroup = reviewGroupRepository.save(new ReviewGroup("aru", "reviewme", "ABCD", "0000")); + Question2 question1 = questionRepository.save(new Question2(true, QuestionType.TEXT, "질문", null, 1)); + Question2 question2 = questionRepository.save(new Question2(true, QuestionType.CHECKBOX, "질문", null, 2)); + Question2 question3 = questionRepository.save(new Question2(true, QuestionType.TEXT, "체크 1 조건", "가이드라인", 3)); + OptionGroup optionGroup = optionGroupRepository.save(new OptionGroup(question2.getId(), 1, 3)); + OptionItem optionItem1 = optionItemRepository.save(new OptionItem("체크 1", optionGroup.getId(), 1, OptionType.KEYWORD)); + OptionItem optionItem2 = optionItemRepository.save(new OptionItem("체크 2", optionGroup.getId(), 2, OptionType.KEYWORD)); + + Section section1 = sectionRepository.save( + new Section(VisibleType.ALWAYS, List.of(question1.getId(), question2.getId()), null, "1번 섹션", 1) + ); + Section section2 = sectionRepository.save( + new Section(VisibleType.CONDITIONAL, List.of(question3.getId()), optionItem1.getId(), "2번 섹션", 2) + ); + Template template = templateRepository.save(new Template(List.of(section1.getId(), section2.getId()))); + + List textAnswers = List.of(new TextAnswer(question1.getId(), "질문 1 답변")); + List checkboxAnswers = List.of( + new CheckboxAnswer(question2.getId(), List.of(optionItem2.getId()))); + Review2 review = reviewRepository2.save( + new Review2(template.getId(), reviewGroup.getId(), textAnswers, checkboxAnswers) + ); + + // when + TemplateAnswerResponse reviewDetail = reviewDetailLookupService.getReviewDetail("0000", review.getId()); + + // then + assertThat(reviewDetail.sections()).hasSize(1); + } +} diff --git a/backend/src/test/java/reviewme/review/service/ReviewServiceTest.java b/backend/src/test/java/reviewme/review/service/ReviewServiceTest.java index f5a8df1e2..5fa630cd6 100644 --- a/backend/src/test/java/reviewme/review/service/ReviewServiceTest.java +++ b/backend/src/test/java/reviewme/review/service/ReviewServiceTest.java @@ -25,7 +25,7 @@ import reviewme.review.domain.Review; import reviewme.review.domain.Review2; import reviewme.review.domain.exception.ReviewGroupNotFoundByGroupAccessCodeException; -import reviewme.review.domain.exception.ReviewIsNotInReviewGroupException; +import reviewme.review.domain.exception.InvalidReviewAccessByReviewGroupException; import reviewme.review.dto.request.CreateReviewContentRequest; import reviewme.review.dto.request.CreateReviewRequest; import reviewme.review.dto.response.QuestionSetupResponse; @@ -169,8 +169,8 @@ class ReviewServiceTest { ); CheckboxAnswer categoryAnswer1 = new CheckboxAnswer(question1.getId(), List.of(categoryOption1.getId())); CheckboxAnswer categoryAnswer2 = new CheckboxAnswer(question1.getId(), List.of(categoryOption2.getId())); - Review2 review1 = new Review2(template.getId(), reviewGroup.getId(), List.of(), List.of(categoryAnswer1), LocalDateTime.now()); - Review2 review2 = new Review2(template.getId(), reviewGroup.getId(), List.of(), List.of(categoryAnswer2), LocalDateTime.now()); + Review2 review1 = new Review2(template.getId(), reviewGroup.getId(), List.of(), List.of(categoryAnswer1)); + Review2 review2 = new Review2(template.getId(), reviewGroup.getId(), List.of(), List.of(categoryAnswer2)); review2Repository.saveAll(List.of(review1, review2)); // when @@ -223,7 +223,7 @@ class ReviewServiceTest { // when, then assertThatThrownBy( () -> reviewService.findReceivedReviewDetail(reviewGroup1.getGroupAccessCode(), review2.getId())) - .isInstanceOf(ReviewIsNotInReviewGroupException.class); + .isInstanceOf(InvalidReviewAccessByReviewGroupException.class); } @Test @@ -271,8 +271,7 @@ class ReviewServiceTest { CheckboxAnswer categoryAnswer = new CheckboxAnswer(question1.getId(), List.of(categoryOption1.getId())); CheckboxAnswer keywordAnswer = new CheckboxAnswer(question2.getId(), List.of(keywordOption.getId())); review2Repository.save( - new Review2(template.getId(), reviewGroup.getId(), List.of(), List.of(categoryAnswer, keywordAnswer), - LocalDateTime.now()) + new Review2(template.getId(), reviewGroup.getId(), List.of(), List.of(categoryAnswer, keywordAnswer)) ); // when diff --git a/backend/src/test/java/reviewme/template/domain/SectionTest.java b/backend/src/test/java/reviewme/template/domain/SectionTest.java new file mode 100644 index 000000000..7757f768f --- /dev/null +++ b/backend/src/test/java/reviewme/template/domain/SectionTest.java @@ -0,0 +1,45 @@ +package reviewme.template.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class SectionTest { + + @Test + void 조건_옵션을_선택하면_섹션이_보인다() { + // given + Section section = new Section(VisibleType.CONDITIONAL, List.of(), 1L, "1", 1); + + // when + boolean actual = section.isVisibleBySelectedOptionIds(List.of(1L, 2L, 3L)); + + // then + assertThat(actual).isTrue(); + } + + @Test + void 조건_옵션을_선택하지_않으면_섹션이_보이지_않는다() { + // given + Section section = new Section(VisibleType.CONDITIONAL, List.of(), 1L, "1", 1); + + // when + boolean actual = section.isVisibleBySelectedOptionIds(List.of(4L, 5L, 6L)); + + // then + assertThat(actual).isFalse(); + } + + @Test + void 타입이_ALWAYS라면_조건과_상관없이_모두_보인다() { + // given + Section section = new Section(VisibleType.ALWAYS, List.of(), null, "1", 1); + + // when + boolean actual = section.isVisibleBySelectedOptionIds(List.of()); + + // then + assertThat(actual).isTrue(); + } +} diff --git a/backend/src/test/java/reviewme/template/repository/SectionRepositoryTest.java b/backend/src/test/java/reviewme/template/repository/SectionRepositoryTest.java new file mode 100644 index 000000000..8eb22ac13 --- /dev/null +++ b/backend/src/test/java/reviewme/template/repository/SectionRepositoryTest.java @@ -0,0 +1,38 @@ +package reviewme.template.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import reviewme.template.domain.Section; +import reviewme.template.domain.Template; +import reviewme.template.domain.VisibleType; + +@DataJpaTest +class SectionRepositoryTest { + + @Autowired + private SectionRepository sectionRepository; + @Autowired + private TemplateRepository templateRepository; + + @Test + void 템플릿_아이디로_섹션을_불러온다() { + // given + Section section1 = sectionRepository.save(new Section(VisibleType.ALWAYS, List.of(), null, "1", 1)); + Section section2 = sectionRepository.save(new Section(VisibleType.ALWAYS, List.of(), null, "2", 1)); + Section section3 = sectionRepository.save(new Section(VisibleType.ALWAYS, List.of(), null, "3", 1)); + sectionRepository.save(new Section(VisibleType.ALWAYS, List.of(), null, "4", 1)); + Template template = templateRepository.save( + new Template(List.of(section1.getId(), section2.getId(), section3.getId())) + ); + + // when + List
actual = sectionRepository.findAllByTemplateId(template.getId()); + + // then + assertThat(actual).containsExactly(section1, section2, section3); + } +}