Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] feat: 리뷰 모아보기 API 구현 #806

Merged
merged 41 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
cdb2784
refactor: dto 이름 변경
nayonsoso Oct 9, 2024
683cd59
feat: 질문ID에 해당하는 Answer 반환 함수 추가
nayonsoso Oct 9, 2024
847b565
feat: 질문ID에 해당하는 OptionItem들 반환 함수 추가
nayonsoso Oct 9, 2024
4d4cf04
feat: 리뷰 요청 코드와 섹션ID에 해당하는 질문 반환 함수 추가
nayonsoso Oct 9, 2024
800345f
feat: 리뷰 모이보기 API 구현
nayonsoso Oct 10, 2024
fb00e55
refactor: 테스트에서 검증하고자 하는 것을 분명히
nayonsoso Oct 10, 2024
a786bf0
refactor: 패키지 의존 방향 통일을 위한 레포지토리 함수 이동
nayonsoso Oct 10, 2024
6a173fc
refactor: 테스트 목적에 해당하는 것만 남기기
nayonsoso Oct 10, 2024
98dd521
refactor: 함수 이름 변경
nayonsoso Oct 10, 2024
a9b265e
refactor: 가동성을 위한 함수 분리
nayonsoso Oct 10, 2024
b4d951e
refactor: 다른 서비스 클래스들과 네이밍 통일
nayonsoso Oct 10, 2024
219dc1a
feat: 리뷰 요청 코드 검증, 섹션 아이디 검증 추가
nayonsoso Oct 10, 2024
97e91ef
refactor: 섹션에 해당하는 질문 가져오는 함수 수정
nayonsoso Oct 10, 2024
a97fe47
refactor: 답변 가져오는 함수 수정
nayonsoso Oct 10, 2024
c7c9c7b
feat: 답변하지 않은 내용은 빈 배열로 받아오도록 하는 기능 추가
nayonsoso Oct 10, 2024
2088cf1
fix: 깨지는 테스트 코드 봉합
nayonsoso Oct 10, 2024
e8f91be
refactor: Mapper 분리
nayonsoso Oct 10, 2024
3da45ef
refactor: 섹션 검증 방법 변경
nayonsoso Oct 10, 2024
be44806
refactor: 필요없는 JOIN 제거
nayonsoso Oct 10, 2024
4fb46d4
test: 테스트 코드 목적별로 분리
nayonsoso Oct 10, 2024
e4e3bdc
feat: 질문 목록을 position 순서대로 정렬
nayonsoso Oct 10, 2024
2bbdc85
refactor: 테스트 코드 extracting 사용 변경
nayonsoso Oct 10, 2024
91a0a82
chore: 오타 수정
nayonsoso Oct 10, 2024
0f3aec6
test: 다른 리뷰 그룹이 있는 상황에 대한 테스트
nayonsoso Oct 10, 2024
a484ec9
refactor: 접근제어자 수정
nayonsoso Oct 10, 2024
bb0c4ea
feat: 하이라이트 응답 추가
nayonsoso Oct 10, 2024
064f71e
test: 추가된 속성 반환 테스트
nayonsoso Oct 10, 2024
970e59c
refactor: 유연한 레포지토리 함수로 변경
nayonsoso Oct 11, 2024
985959d
refactor: 변수명 변경
nayonsoso Oct 11, 2024
c80f961
refactor: 가독성 개선
nayonsoso Oct 11, 2024
c490a20
refactor: Answer -> 구체Answer 캐스팅 예외 추가
nayonsoso Oct 11, 2024
50c2126
refactor: 서술형 답변 가져오는 함수 수정
nayonsoso Oct 11, 2024
3725edf
refactor: 선택지 목록 가져오는 함수 수정
nayonsoso Oct 11, 2024
e37bda3
test: reviewGatherMapper에 대한 테스트 작성
nayonsoso Oct 11, 2024
3eea7eb
style: 개행
nayonsoso Oct 11, 2024
196f309
chore: 로그 메세지 수정
nayonsoso Oct 11, 2024
1f9aec2
refactor: 컨트롤러 인자 타입 변경
nayonsoso Oct 11, 2024
8905a13
feat: 최대로 내려주는 응답 수 제한 기능 구현
nayonsoso Oct 11, 2024
e8f0a40
refactor: 테스트 코드 필드 접근제한자 추가
nayonsoso Oct 11, 2024
d202f58
refactor: 변수명 변경
nayonsoso Oct 11, 2024
8a8ce13
chore: 사용하지 않는 메서드 제거
nayonsoso Oct 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Response 접미사를 빼면 남는 건 Text인데, 맥락 전달이 부족하지 않을까요? 기존 AnswerContent에서 바뀌게 된 이유가 무엇일까용?

Copy link
Contributor Author

@nayonsoso nayonsoso Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answer 라는 이름의 도메인이 있었기 때문이에요.

서비스 코드에서 Answer 도메인과 AnswerContentResponse 를 함께 사용하다보니AnswerContentResponse 이 기존의 의미인 "서술형 답변" 이라기보다
Answer 자체에 더 가깝다고 느껴졌어요!


@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 {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확실히 조회 기능별로 mapper 생기는 건 점점 무섭긴 하네요..

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