diff --git a/backend/src/main/java/reviewme/review/controller/ReviewController.java b/backend/src/main/java/reviewme/review/controller/ReviewController.java index c17d2d5f3..da2f62749 100644 --- a/backend/src/main/java/reviewme/review/controller/ReviewController.java +++ b/backend/src/main/java/reviewme/review/controller/ReviewController.java @@ -6,12 +6,14 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import reviewme.review.dto.request.CreateReviewRequest; import reviewme.review.dto.response.ReceivedReviewsResponse; +import reviewme.review.dto.response.ReviewDetailResponse; import reviewme.review.dto.response.ReviewSetupResponse; import reviewme.review.service.ReviewService; @@ -19,6 +21,8 @@ @RequiredArgsConstructor public class ReviewController implements ReviewApi { + private static final String GROUP_ACCESS_CODE_HEADER = "GroupAccessCode"; + private final ReviewService reviewService; @PostMapping("/reviews") @@ -29,7 +33,7 @@ public ResponseEntity createReview(@Valid @RequestBody CreateReviewRequest @GetMapping("/reviews") public ResponseEntity findReceivedReviews(HttpServletRequest request) { - String groupAccessCode = request.getHeader("GroupAccessCode"); + String groupAccessCode = request.getHeader(GROUP_ACCESS_CODE_HEADER); ReceivedReviewsResponse response = reviewService.findReceivedReviews(groupAccessCode); return ResponseEntity.ok(response); } @@ -39,4 +43,11 @@ public ResponseEntity findReviewCreationSetup(@RequestParam ReviewSetupResponse response = reviewService.findReviewCreationSetup(reviewRequestCode); return ResponseEntity.ok(response); } + + @GetMapping("/reviews/{id}") + public ResponseEntity findReceivedReviewDetail(@PathVariable long id, HttpServletRequest request) { + String groupAccessCode = request.getHeader(GROUP_ACCESS_CODE_HEADER); + ReviewDetailResponse response = reviewService.findReceivedReviewDetail(groupAccessCode, id); + return ResponseEntity.ok(response); + } } diff --git a/backend/src/main/java/reviewme/review/domain/Review.java b/backend/src/main/java/reviewme/review/domain/Review.java index 7818f86a7..d1456eae1 100644 --- a/backend/src/main/java/reviewme/review/domain/Review.java +++ b/backend/src/main/java/reviewme/review/domain/Review.java @@ -41,4 +41,8 @@ public Review(long reviewGroupId, List reviewContents, LocalDateT this.reviewContents = reviewContents; this.createdAt = createdAt; } + + public boolean isGroupIdEqualTo(long reviewGroupId) { + return this.reviewGroupId == reviewGroupId; + } } diff --git a/backend/src/main/java/reviewme/review/domain/exception/ReviewGroupNotFoundException.java b/backend/src/main/java/reviewme/review/domain/exception/ReviewGroupNotFoundException.java new file mode 100644 index 000000000..e9aef5620 --- /dev/null +++ b/backend/src/main/java/reviewme/review/domain/exception/ReviewGroupNotFoundException.java @@ -0,0 +1,10 @@ +package reviewme.review.domain.exception; + +import reviewme.global.exception.NotFoundException; + +public class ReviewGroupNotFoundException extends NotFoundException { + + public ReviewGroupNotFoundException() { + super("리뷰 그룹을 찾을 수 없어요."); + } +} diff --git a/backend/src/main/java/reviewme/review/domain/exception/ReviewIsNotInReviewGroupException.java b/backend/src/main/java/reviewme/review/domain/exception/ReviewIsNotInReviewGroupException.java new file mode 100644 index 000000000..020f40ea9 --- /dev/null +++ b/backend/src/main/java/reviewme/review/domain/exception/ReviewIsNotInReviewGroupException.java @@ -0,0 +1,10 @@ +package reviewme.review.domain.exception; + +import reviewme.global.exception.BadRequestException; + +public class ReviewIsNotInReviewGroupException extends BadRequestException { + + public ReviewIsNotInReviewGroupException() { + super("리뷰 그룹에 해당하는 리뷰가 아니에요."); + } +} diff --git a/backend/src/main/java/reviewme/review/dto/response/ReviewSetUpKeyword.java b/backend/src/main/java/reviewme/review/dto/response/KeywordResponse.java similarity index 88% rename from backend/src/main/java/reviewme/review/dto/response/ReviewSetUpKeyword.java rename to backend/src/main/java/reviewme/review/dto/response/KeywordResponse.java index ed28d4060..e8edf0159 100644 --- a/backend/src/main/java/reviewme/review/dto/response/ReviewSetUpKeyword.java +++ b/backend/src/main/java/reviewme/review/dto/response/KeywordResponse.java @@ -3,7 +3,7 @@ import io.swagger.v3.oas.annotations.media.Schema; @Schema(name = "키워드 응답") -public record ReviewSetUpKeyword( +public record KeywordResponse( @Schema(description = "키워드 ID") long id, diff --git a/backend/src/main/java/reviewme/review/dto/response/ReviewDetailResponse.java b/backend/src/main/java/reviewme/review/dto/response/ReviewDetailResponse.java new file mode 100644 index 000000000..475ecb3a1 --- /dev/null +++ b/backend/src/main/java/reviewme/review/dto/response/ReviewDetailResponse.java @@ -0,0 +1,28 @@ +package reviewme.review.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDate; +import java.util.List; + +@Schema(name = "리뷰 상세 조회 응답") +public record ReviewDetailResponse( + + @Schema(description = "리뷰 ID") + long id, + + @Schema(description = "리뷰 작성일") + LocalDate createdAt, + + @Schema(description = "프로젝트명") + String projectName, + + @Schema(description = "리뷰이 이름") + String revieweeName, + + @Schema(description = "리뷰 문항 목록") + List contents, + + @Schema(description = "키워드 목록") + List keywords +) { +} diff --git a/backend/src/main/java/reviewme/review/dto/response/ReviewSetupResponse.java b/backend/src/main/java/reviewme/review/dto/response/ReviewSetupResponse.java index d11b922d7..49fa07432 100644 --- a/backend/src/main/java/reviewme/review/dto/response/ReviewSetupResponse.java +++ b/backend/src/main/java/reviewme/review/dto/response/ReviewSetupResponse.java @@ -16,6 +16,6 @@ public record ReviewSetupResponse( List questions, @Schema(description = "키워드 목록") - List keywords + List keywords ) { } diff --git a/backend/src/main/java/reviewme/review/repository/ReviewRepository.java b/backend/src/main/java/reviewme/review/repository/ReviewRepository.java index cbf1ed91d..83feef256 100644 --- a/backend/src/main/java/reviewme/review/repository/ReviewRepository.java +++ b/backend/src/main/java/reviewme/review/repository/ReviewRepository.java @@ -1,6 +1,7 @@ package reviewme.review.repository; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import reviewme.review.domain.Review; @@ -11,6 +12,8 @@ public interface ReviewRepository extends JpaRepository { List findAllByReviewGroupId(long id); + Optional findByIdAndReviewGroupId(long reviewId, long reviewGroupId); + default Review getReviewById(Long id) { return findById(id).orElseThrow(ReviewNotFoundException::new); } diff --git a/backend/src/main/java/reviewme/review/service/ReviewService.java b/backend/src/main/java/reviewme/review/service/ReviewService.java index 041876f78..61b6e0354 100644 --- a/backend/src/main/java/reviewme/review/service/ReviewService.java +++ b/backend/src/main/java/reviewme/review/service/ReviewService.java @@ -6,23 +6,26 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import reviewme.keyword.repository.KeywordRepository; +import reviewme.question.domain.Question; import reviewme.review.domain.Review; import reviewme.review.domain.ReviewContent; import reviewme.review.domain.ReviewKeyword; +import reviewme.review.domain.exception.ReviewGroupNotFoundException; +import reviewme.review.domain.exception.ReviewIsNotInReviewGroupException; import reviewme.review.dto.request.CreateReviewContentRequest; import reviewme.review.dto.request.CreateReviewRequest; +import reviewme.review.dto.response.KeywordResponse; import reviewme.review.dto.response.QuestionSetupResponse; import reviewme.review.dto.response.ReceivedReviewKeywordsResponse; import reviewme.review.dto.response.ReceivedReviewResponse; import reviewme.review.dto.response.ReceivedReviewsResponse; -import reviewme.review.dto.response.ReviewSetUpKeyword; +import reviewme.review.dto.response.ReviewContentResponse; +import reviewme.review.dto.response.ReviewDetailResponse; import reviewme.review.dto.response.ReviewSetupResponse; import reviewme.review.repository.QuestionRepository; import reviewme.review.repository.ReviewContentRepository; import reviewme.review.repository.ReviewKeywordRepository; import reviewme.review.repository.ReviewRepository; -import reviewme.review.service.exception.InvalidGroupAccessCodeException; -import reviewme.review.service.exception.InvalidReviewRequestCodeException; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; @@ -51,7 +54,7 @@ public Long createReview(CreateReviewRequest request) { private Review saveReview(CreateReviewRequest request) { ReviewGroup reviewGroup = reviewGroupRepository.findByReviewRequestCode(request.reviewRequestCode()) - .orElseThrow(InvalidReviewRequestCodeException::new); + .orElseThrow(ReviewGroupNotFoundException::new); List questionIds = request.reviewContents() .stream() @@ -79,29 +82,67 @@ private void saveReviewKeywords(List selectedKeywordIds, long savedReviewI @Transactional(readOnly = true) public ReviewSetupResponse findReviewCreationSetup(String reviewRequestCode) { ReviewGroup reviewGroup = reviewGroupRepository.findByReviewRequestCode(reviewRequestCode) - .orElseThrow(InvalidReviewRequestCodeException::new); + .orElseThrow(ReviewGroupNotFoundException::new); return createReviewSetupResponse(reviewGroup); } private ReviewSetupResponse createReviewSetupResponse(ReviewGroup reviewGroup) { - List questionSetupRespons = questionRepository.findAll() + List questionSetupResponse = questionRepository.findAll() .stream() .map(question -> new QuestionSetupResponse(question.getId(), question.getContent())) .toList(); - List reviewSetUpKeywordRespons = keywordRepository.findAll() + List keywordResponse = keywordRepository.findAll() .stream() - .map(keyword -> new ReviewSetUpKeyword(keyword.getId(), keyword.getContent())) + .map(keyword -> new KeywordResponse(keyword.getId(), keyword.getContent())) .toList(); - return new ReviewSetupResponse(reviewGroup.getReviewee(), reviewGroup.getProjectName(), - questionSetupRespons, reviewSetUpKeywordRespons); + return new ReviewSetupResponse( + reviewGroup.getReviewee(), reviewGroup.getProjectName(), questionSetupResponse, keywordResponse + ); + } + + @Transactional(readOnly = true) + public ReviewDetailResponse findReceivedReviewDetail(String groupAccessCode, long reviewId) { + ReviewGroup reviewGroup = reviewGroupRepository.findByGroupAccessCode(groupAccessCode) + .orElseThrow(ReviewGroupNotFoundException::new); + + Review review = reviewRepository.findByIdAndReviewGroupId(reviewId, reviewGroup.getId()) + .orElseThrow(ReviewIsNotInReviewGroupException::new); + + return createReviewDetailResponse(review, reviewGroup); + } + + private ReviewDetailResponse createReviewDetailResponse(Review review, ReviewGroup reviewGroup) { + List reviewContents = review.getReviewContents() + .stream() + .map(reviewContent -> { + Question question = questionRepository.getQuestionById(reviewContent.getQuestionId()); + return new ReviewContentResponse(reviewContent.getId(), question.getContent(), + reviewContent.getAnswer()); + }) + .toList(); + + List keywords = reviewKeywordRepository.findAllByReviewId(review.getId()) + .stream() + .map(reviewKeyword -> keywordRepository.getKeywordById(reviewKeyword.getKeywordId())) + .map(keyword -> new KeywordResponse(keyword.getId(), keyword.getContent())) + .toList(); + + return new ReviewDetailResponse( + review.getId(), + review.getCreatedAt().toLocalDate(), + reviewGroup.getProjectName(), + reviewGroup.getReviewee(), + reviewContents, + keywords + ); } @Transactional(readOnly = true) public ReceivedReviewsResponse findReceivedReviews(String groupAccessCode) { ReviewGroup reviewGroup = reviewGroupRepository.findByGroupAccessCode(groupAccessCode) - .orElseThrow(InvalidGroupAccessCodeException::new); + .orElseThrow(ReviewGroupNotFoundException::new); List reviewResponses = reviewRepository.findAllByReviewGroupId(reviewGroup.getId()) .stream() .map(this::extractResponse) diff --git a/backend/src/main/java/reviewme/review/service/exception/InvalidReviewRequestCodeException.java b/backend/src/main/java/reviewme/review/service/exception/InvalidReviewRequestCodeException.java index 396b9f980..e69de29bb 100644 --- a/backend/src/main/java/reviewme/review/service/exception/InvalidReviewRequestCodeException.java +++ b/backend/src/main/java/reviewme/review/service/exception/InvalidReviewRequestCodeException.java @@ -1,10 +0,0 @@ -package reviewme.review.service.exception; - -import reviewme.global.exception.BadRequestException; - -public class InvalidReviewRequestCodeException extends BadRequestException { - - public InvalidReviewRequestCodeException() { - super("잘못된 리뷰 요청코드입니다."); - } -} diff --git a/backend/src/test/java/reviewme/review/service/ReviewServiceTest.java b/backend/src/test/java/reviewme/review/service/ReviewServiceTest.java index 092bc00df..e5c5d3663 100644 --- a/backend/src/test/java/reviewme/review/service/ReviewServiceTest.java +++ b/backend/src/test/java/reviewme/review/service/ReviewServiceTest.java @@ -20,9 +20,12 @@ import reviewme.review.domain.Review; import reviewme.review.domain.ReviewContent; import reviewme.review.domain.ReviewKeyword; +import reviewme.review.domain.exception.ReviewGroupNotFoundException; +import reviewme.review.domain.exception.ReviewIsNotInReviewGroupException; import reviewme.review.dto.request.CreateReviewContentRequest; import reviewme.review.dto.request.CreateReviewRequest; import reviewme.review.dto.response.ReceivedReviewsResponse; +import reviewme.review.dto.response.ReviewDetailResponse; import reviewme.review.dto.response.ReviewSetupResponse; import reviewme.review.repository.QuestionRepository; import reviewme.review.repository.ReviewContentRepository; @@ -116,7 +119,7 @@ class ReviewServiceTest { @Test void 확인_코드에_해당하는_그룹이_없는_경우_예외가_발생한다() { assertThatThrownBy(() -> reviewService.findReceivedReviews("abc")) - .isInstanceOf(InvalidGroupAccessCodeException.class); + .isInstanceOf(ReviewGroupNotFoundException.class); } @Test @@ -147,4 +150,50 @@ class ReviewServiceTest { // then assertThat(response.reviews()).hasSize(2); } + + @Test + void 리뷰를_조회한다() { + // given + String groupAccessCode = "groupAccessCode"; + ReviewGroup reviewGroup = reviewGroupRepository.save( + new ReviewGroup("테드", "리뷰미 프로젝트", "reviewRequestCode", groupAccessCode)); + Review review = reviewRepository.save(new Review(reviewGroup.getId(), List.of(), LocalDateTime.now())); + + // when + ReviewDetailResponse response = reviewService.findReceivedReviewDetail(groupAccessCode, + review.getId()); + + // then + assertThat(response.id()).isEqualTo(review.getId()); + } + + @Test + void 잘못된_그룹_액세스_코드로_리뷰를_조회할_경우_예외를_발생한다() { + // given + ReviewGroup reviewGroup = reviewGroupRepository.save( + new ReviewGroup("테드", "리뷰미 프로젝트", "reviewRequestCode", "groupAccessCode")); + + Review review = reviewRepository.save(new Review(reviewGroup.getId(), List.of(), LocalDateTime.now())); + + // when, then + assertThatThrownBy(() -> reviewService.findReceivedReviewDetail("wrongGroupAccessCode", review.getId())) + .isInstanceOf(ReviewGroupNotFoundException.class); + } + + @Test + void 리뷰_그룹에_해당하는_않는_리뷰를_조회할_경우_예외를_발생한다() { + // given + ReviewGroup reviewGroup1 = reviewGroupRepository.save( + new ReviewGroup("테드", "리뷰미 프로젝트", "reviewRequestCode1", "groupAccessCode1")); + ReviewGroup reviewGroup2 = reviewGroupRepository.save( + new ReviewGroup("테드", "리뷰미 프로젝트", "reviewRequestCode2", "groupAccessCode2")); + + Review review1 = reviewRepository.save(new Review(reviewGroup1.getId(), List.of(), LocalDateTime.now())); + Review review2 = reviewRepository.save(new Review(reviewGroup2.getId(), List.of(), LocalDateTime.now())); + + // when, then + assertThatThrownBy( + () -> reviewService.findReceivedReviewDetail(reviewGroup1.getGroupAccessCode(), review2.getId())) + .isInstanceOf(ReviewIsNotInReviewGroupException.class); + } }