Skip to content

Commit

Permalink
[BE] feat: 리뷰 모아보기 API 구현 (#806)
Browse files Browse the repository at this point in the history
* refactor: dto 이름 변경

AnswerContentResponse -> TextResponse

* feat: 질문ID에 해당하는 Answer 반환 함수 추가

* feat: 질문ID에 해당하는 OptionItem들 반환 함수 추가

* feat: 리뷰 요청 코드와 섹션ID에 해당하는 질문 반환 함수 추가

* feat: 리뷰 모이보기 API 구현

* refactor: 테스트에서 검증하고자 하는 것을 분명히

* refactor: 패키지 의존 방향 통일을 위한 레포지토리 함수 이동

* refactor: 테스트 목적에 해당하는 것만 남기기

* refactor: 함수 이름 변경

- 인자가 List<Long> 이므로, ids 로 끝나게 변경함

* refactor: 가동성을 위한 함수 분리

* refactor: 다른 서비스 클래스들과 네이밍 통일

* feat: 리뷰 요청 코드 검증, 섹션 아이디 검증 추가

* refactor: 섹션에 해당하는 질문 가져오는 함수 수정

AS-IS: 내 리뷰그룹 중 특정 세션에 대한 질문 가져오기
TO-BE: 리뷰그룹 검증 완료되었으므로 특정 세션에 대해서만 가져오도록 수정

* refactor: 답변 가져오는 함수 수정

AS-IS: 특정 질문에 대해서 답변된 것 다 받아오기
TO-BE: 내 리뷰 그룹에 해당한다는 조건 추가

* feat: 답변하지 않은 내용은 빈 배열로 받아오도록 하는 기능 추가

* fix: 깨지는 테스트 코드 봉합

* refactor: Mapper 분리

* refactor: 섹션 검증 방법 변경

AS-IS: boolean 으로 검증, sectionId 인자 그대로 사용
TO-BE: Optional<Section>으로 검증, section.getId() 사용

* refactor: 필요없는 JOIN 제거

* test: 테스트 코드 목적별로 분리

* feat: 질문 목록을 position 순서대로 정렬

* refactor: 테스트 코드 extracting 사용 변경

AS-IS: attribute 값을 문자열로 하드코딩
TO-BE: dtp 의 필드명으로 가져오기 e.g. VoteResponse::content

* chore: 오타 수정

* test: 다른 리뷰 그룹이 있는 상황에 대한 테스트

* refactor: 접근제어자 수정

* feat: 하이라이트 응답 추가

* test: 추가된 속성 반환 테스트

* refactor: 유연한 레포지토리 함수로 변경

* refactor: 변수명 변경

* refactor: 가독성 개선

함수 분리, 변수명 변경, 함수 인자 변경

* refactor: Answer -> 구체Answer 캐스팅 예외 추가

* refactor: 서술형 답변 가져오는 함수 수정

* refactor: 선택지 목록 가져오는 함수 수정

* test: reviewGatherMapper에 대한 테스트 작성

* style: 개행

- 가독성 개선와 컨벤션 유지를 위함

* chore: 로그 메세지 수정

- 컨벤션 유지를 위함

* refactor: 컨트롤러 인자 타입 변경

- Long -> long

* feat: 최대로 내려주는 응답 수 제한 기능 구현

* refactor: 테스트 코드 필드 접근제한자 추가

* refactor: 변수명 변경

* chore: 사용하지 않는 메서드 제거
  • Loading branch information
nayonsoso authored Oct 11, 2024
1 parent ee2c950 commit 7e5564b
Show file tree
Hide file tree
Showing 24 changed files with 954 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
import java.util.Set;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import reviewme.question.domain.OptionItem;
import reviewme.question.domain.Question;

@Repository
public interface QuestionRepository extends JpaRepository<Question, Long> {

@Query(value = """
Expand All @@ -27,4 +30,20 @@ public interface QuestionRepository extends JpaRepository<Question, Long> {
WHERE ts.template_id = :templateId
""", nativeQuery = true)
List<Question> findAllByTemplatedId(long templateId);

@Query(value = """
SELECT q FROM Question q
JOIN SectionQuestion sq ON q.id = sq.questionId
WHERE sq.sectionId = :sectionId
ORDER BY q.position
""")
List<Question> findAllBySectionIdOrderByPosition(long sectionId);

@Query("""
SELECT o FROM OptionItem o
JOIN OptionGroup og ON o.optionGroupId = og.id
WHERE og.questionId = :questionId
ORDER BY o.position
""")
List<OptionItem> findAllOptionItemsByIdOrderByPosition(long questionId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.SessionAttribute;
import reviewme.review.service.GatheredReviewLookupService;
import reviewme.review.service.ReviewGatheredLookupService;
import reviewme.review.service.ReviewDetailLookupService;
import reviewme.review.service.ReviewListLookupService;
import reviewme.review.service.ReviewRegisterService;
Expand All @@ -30,7 +30,7 @@ public class ReviewController {
private final ReviewListLookupService reviewListLookupService;
private final ReviewDetailLookupService reviewDetailLookupService;
private final ReviewSummaryService reviewSummaryService;
private final GatheredReviewLookupService gatheredReviewLookupService;
private final ReviewGatheredLookupService reviewGatheredLookupService;

@PostMapping("/v2/reviews")
public ResponseEntity<Void> createReview(@Valid @RequestBody ReviewRegisterRequest request) {
Expand Down Expand Up @@ -68,10 +68,10 @@ public ResponseEntity<ReceivedReviewsSummaryResponse> findReceivedReviewOverview

@GetMapping("/v2/reviews/gather")
public ResponseEntity<ReviewsGatheredBySectionResponse> getReceivedReviewsBySectionId(
@RequestParam("sectionId") Long sectionId,
@RequestParam("sectionId") long sectionId,
@SessionAttribute("reviewRequestCode") String reviewRequestCode
) {
ReviewsGatheredBySectionResponse response = gatheredReviewLookupService.getReceivedReviewsBySectionId(
ReviewsGatheredBySectionResponse response = reviewGatheredLookupService.getReceivedReviewsBySectionId(
reviewRequestCode, sectionId);
return ResponseEntity.ok(response);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package reviewme.review.repository;

import java.util.Collection;
import java.util.List;
import java.util.Set;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
Expand All @@ -9,6 +11,15 @@
@Repository
public interface AnswerRepository extends JpaRepository<Answer, Long> {

@Query(value = """
SELECT a FROM Answer a
JOIN Review r ON a.reviewId = r.id
WHERE r.reviewGroupId = :reviewGroupId AND a.questionId IN :questionIds
ORDER BY r.createdAt DESC
LIMIT :limit
""")
List<Answer> findReceivedAnswersByQuestionIds(long reviewGroupId, Collection<Long> questionIds, int limit);

@Query(value = """
SELECT a.id FROM Answer a
JOIN Review r
Expand All @@ -19,17 +30,15 @@ public interface AnswerRepository extends JpaRepository<Answer, Long> {

@Query(value = """
SELECT a FROM Answer a
JOIN Review r
ON a.reviewId = r.id
WHERE r.reviewGroupId = :reviewGroupId
JOIN Review r
ON a.reviewId = r.id
WHERE r.reviewGroupId = :reviewGroupId
""")
Set<Answer> findAllByReviewGroupId(long reviewGroupId);

@Query(value = """
SELECT a.id FROM Answer a
JOIN Question q
ON a.questionId = q.id
WHERE q.id = :questionId
WHERE a.questionId = :questionId
""")
Set<Long> findIdsByQuestionId(long questionId);
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package reviewme.review.service;

import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reviewme.question.domain.Question;
import reviewme.question.repository.QuestionRepository;
import reviewme.review.domain.Answer;
import reviewme.review.repository.AnswerRepository;
import reviewme.review.service.dto.response.gathered.ReviewsGatheredBySectionResponse;
import reviewme.review.service.exception.ReviewGroupNotFoundByReviewRequestCodeException;
import reviewme.review.service.exception.SectionNotFoundInTemplateException;
import reviewme.review.service.mapper.ReviewGatherMapper;
import reviewme.reviewgroup.domain.ReviewGroup;
import reviewme.reviewgroup.repository.ReviewGroupRepository;
import reviewme.template.domain.Section;
import reviewme.template.repository.SectionRepository;

@Service
@RequiredArgsConstructor
public class ReviewGatheredLookupService {

private static final int ANSWER_RESPONSE_LIMIT = 100;

private final QuestionRepository questionRepository;
private final AnswerRepository answerRepository;
private final ReviewGroupRepository reviewGroupRepository;
private final SectionRepository sectionRepository;

private final ReviewGatherMapper reviewGatherMapper;

@Transactional(readOnly = true)
public ReviewsGatheredBySectionResponse getReceivedReviewsBySectionId(String reviewRequestCode, long sectionId) {
ReviewGroup reviewGroup = getReviewGroupOrThrow(reviewRequestCode);
Section section = getSectionOrThrow(sectionId, reviewGroup);
Map<Question, List<Answer>> questionAnswers = getQuestionAnswers(section, reviewGroup);

return reviewGatherMapper.mapToReviewsGatheredBySection(questionAnswers);
}

private ReviewGroup getReviewGroupOrThrow(String reviewRequestCode) {
return reviewGroupRepository.findByReviewRequestCode(reviewRequestCode)
.orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode));
}

private Section getSectionOrThrow(long sectionId, ReviewGroup reviewGroup) {
return sectionRepository.findByIdAndTemplateId(sectionId, reviewGroup.getTemplateId())
.orElseThrow(() -> new SectionNotFoundInTemplateException(sectionId, reviewGroup.getTemplateId()));
}

private Map<Question, List<Answer>> getQuestionAnswers(Section section, ReviewGroup reviewGroup) {
Map<Long, Question> questionIdQuestion = questionRepository
.findAllBySectionIdOrderByPosition(section.getId())
.stream()
.collect(Collectors.toMap(Question::getId, Function.identity()));

Map<Long, List<Answer>> questionIdAnswers = answerRepository
.findReceivedAnswersByQuestionIds(reviewGroup.getId(), questionIdQuestion.keySet(),
ANSWER_RESPONSE_LIMIT)
.stream()
.collect(Collectors.groupingBy(Answer::getQuestionId));

return questionIdQuestion.values().stream()
.collect(Collectors.toMap(
Function.identity(),
question -> questionIdAnswers.getOrDefault(question.getId(), List.of())
));
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package reviewme.review.service.dto.response.gathered;

import java.util.List;

public record HighlightResponse(
long lineIndex,
List<RangeResponse> ranges
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package reviewme.review.service.dto.response.gathered;

public record RangeResponse(
long startIndex,
long endIndex
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public record ReviewsGatheredByQuestionResponse(
SimpleQuestionResponse question,

@Nullable
List<AnswerContentResponse> answers,
List<TextResponse> answers,

@Nullable
List<VoteResponse> votes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import reviewme.question.domain.QuestionType;

public record SimpleQuestionResponse(
long id,
String name,
QuestionType type
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package reviewme.review.service.dto.response.gathered;

import java.util.List;

public record TextResponse(
long id,
String content,
List<HighlightResponse> highlights
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

public record VoteResponse(
String content,
int count
long count
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package reviewme.review.service.exception;

import lombok.extern.slf4j.Slf4j;
import reviewme.global.exception.DataInconsistencyException;

@Slf4j
public class GatheredAnswersTypeNonUniformException extends DataInconsistencyException {

public GatheredAnswersTypeNonUniformException(Throwable cause) {
super("서버 내부 오류가 발생했습니다.");
log.error("The types of answers to questions are not uniform.", cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package reviewme.review.service.exception;

import lombok.extern.slf4j.Slf4j;
import reviewme.global.exception.NotFoundException;

@Slf4j
public class SectionNotFoundInTemplateException extends NotFoundException {

public SectionNotFoundInTemplateException(long sectionId, long templateId) {
super("섹션 정보를 찾을 수 없습니다.");
log.info("Section not found in template - sectionId: {}, templateId: {}", sectionId, templateId, this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package reviewme.review.service.mapper;

import jakarta.annotation.Nullable;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import reviewme.question.domain.OptionItem;
import reviewme.question.domain.Question;
import reviewme.question.repository.QuestionRepository;
import reviewme.review.domain.Answer;
import reviewme.review.domain.CheckboxAnswer;
import reviewme.review.domain.CheckboxAnswerSelectedOption;
import reviewme.review.domain.TextAnswer;
import reviewme.review.service.dto.response.gathered.ReviewsGatheredByQuestionResponse;
import reviewme.review.service.dto.response.gathered.ReviewsGatheredBySectionResponse;
import reviewme.review.service.dto.response.gathered.SimpleQuestionResponse;
import reviewme.review.service.dto.response.gathered.TextResponse;
import reviewme.review.service.dto.response.gathered.VoteResponse;
import reviewme.review.service.exception.GatheredAnswersTypeNonUniformException;

@Component
@RequiredArgsConstructor
public class ReviewGatherMapper {

private final QuestionRepository questionRepository;

public ReviewsGatheredBySectionResponse mapToReviewsGatheredBySection(Map<Question, List<Answer>> questionAnswers) {
List<ReviewsGatheredByQuestionResponse> reviews = questionAnswers.entrySet()
.stream()
.map(entry -> mapToReviewsGatheredByQuestion(entry.getKey(), entry.getValue()))
.toList();

return new ReviewsGatheredBySectionResponse(reviews);
}

private ReviewsGatheredByQuestionResponse mapToReviewsGatheredByQuestion(Question question, List<Answer> answers) {
return new ReviewsGatheredByQuestionResponse(
new SimpleQuestionResponse(question.getId(), question.getContent(), question.getQuestionType()),
mapToTextResponse(question, answers),
mapToVoteResponse(question, answers)
);
}

@Nullable
private List<TextResponse> mapToTextResponse(Question question, List<Answer> answers) {
if (question.isSelectable()) {
return null;
}

List<TextAnswer> textAnswers = castAllOrThrow(answers, TextAnswer.class);
return textAnswers.stream()
.map(textAnswer -> new TextResponse(textAnswer.getId(), textAnswer.getContent(), List.of()))
.toList();
}

@Nullable
private List<VoteResponse> mapToVoteResponse(Question question, List<Answer> answers) {
if (!question.isSelectable()) {
return null;
}

List<CheckboxAnswer> checkboxAnswers = castAllOrThrow(answers, CheckboxAnswer.class);
Map<Long, Long> optionItemIdVoteCount = checkboxAnswers.stream()
.flatMap(checkboxAnswer -> checkboxAnswer.getSelectedOptionIds().stream())
.collect(Collectors.groupingBy(CheckboxAnswerSelectedOption::getSelectedOptionId,
Collectors.counting()));

List<OptionItem> allOptionItem = questionRepository.findAllOptionItemsByIdOrderByPosition(question.getId());
return allOptionItem.stream()
.map(optionItem -> new VoteResponse(
optionItem.getContent(),
optionItemIdVoteCount.getOrDefault(optionItem.getId(), 0L)))
.toList();
}

private <T extends Answer> List<T> castAllOrThrow(List<Answer> answers, Class<T> clazz) {
try {
return answers.stream().map(clazz::cast).toList();
} catch (Exception ex) {
throw new GatheredAnswersTypeNonUniformException(ex);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,17 @@ public class ReviewGroup {
private long templateId = 1L;

public ReviewGroup(String reviewee, String projectName, String reviewRequestCode, String groupAccessCode) {
this(reviewee, projectName, reviewRequestCode, groupAccessCode, 1L);
}

public ReviewGroup(String reviewee, String projectName, String reviewRequestCode, String groupAccessCode, long templateId) {
validateRevieweeLength(reviewee);
validateProjectNameLength(projectName);
this.reviewee = reviewee;
this.projectName = projectName;
this.reviewRequestCode = reviewRequestCode;
this.groupAccessCode = new GroupAccessCode(groupAccessCode);
this.templateId = templateId;
}

private void validateRevieweeLength(String reviewee) {
Expand Down
Loading

0 comments on commit 7e5564b

Please sign in to comment.