diff --git a/src/main/java/com/server/running_handai/domain/review/controller/ReviewController.java b/src/main/java/com/server/running_handai/domain/review/controller/ReviewController.java index a95ac8d..5034c1a 100644 --- a/src/main/java/com/server/running_handai/domain/review/controller/ReviewController.java +++ b/src/main/java/com/server/running_handai/domain/review/controller/ReviewController.java @@ -1,5 +1,6 @@ package com.server.running_handai.domain.review.controller; +import com.server.running_handai.domain.review.dto.MyReviewInfoDto; import com.server.running_handai.domain.review.dto.ReviewCreateResponseDto; import com.server.running_handai.domain.review.dto.ReviewInfoListDto; import com.server.running_handai.domain.review.dto.ReviewCreateRequestDto; @@ -14,9 +15,9 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springdoc.core.annotations.ParameterObject; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; @@ -104,4 +105,23 @@ public ResponseEntity> deleteReview( return ResponseEntity.ok(CommonResponse.success(ResponseCode.SUCCESS, null)); } + @Operation(summary = "내 리뷰 조회", description = "회원이 작성한 리뷰를 조회합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "성공"), + @ApiResponse(responseCode = "200", description = "성공 (리뷰 없음)") + }) + @GetMapping("/api/members/me/reviews") + public ResponseEntity>> getMyReviews( + @AuthenticationPrincipal CustomOAuth2User customOAuth2User + ) { + Long memberId = customOAuth2User.getMember().getId(); + List responseData = reviewService.getMyReviews(memberId); + + if (responseData.isEmpty()) { + return ResponseEntity.ok(CommonResponse.success(ResponseCode.SUCCESS_EMPTY_REVIEWS, responseData)); + } + + return ResponseEntity.ok(CommonResponse.success(ResponseCode.SUCCESS, responseData)); + } + } diff --git a/src/main/java/com/server/running_handai/domain/review/dto/MyReviewInfoDto.java b/src/main/java/com/server/running_handai/domain/review/dto/MyReviewInfoDto.java new file mode 100644 index 0000000..129edec --- /dev/null +++ b/src/main/java/com/server/running_handai/domain/review/dto/MyReviewInfoDto.java @@ -0,0 +1,39 @@ +package com.server.running_handai.domain.review.dto; + +import com.server.running_handai.domain.course.entity.Course; +import com.server.running_handai.domain.review.entity.Review; +import java.time.format.DateTimeFormatter; + +public record MyReviewInfoDto( + long reviewId, + long courseId, + String courseName, + String thumbnailUrl, + String area, + double distance, + int duration, + int maxElevation, + double stars, + String contents, + String createdAt +) { + public static MyReviewInfoDto from(Review review) { + Course course = review.getCourse(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + String formattedCreatedAt = review.getCreatedAt().format(formatter); + + return new MyReviewInfoDto( + review.getId(), + course.getId(), + course.getName(), + (course.getCourseImage() != null) ? course.getCourseImage().getImgUrl() : null, + course.getArea().name(), + course.getDistance(), + course.getDuration(), + (int) course.getMaxElevation().doubleValue(), + review.getStars(), + review.getContents(), + formattedCreatedAt + ); + } +} diff --git a/src/main/java/com/server/running_handai/domain/review/repository/ReviewRepository.java b/src/main/java/com/server/running_handai/domain/review/repository/ReviewRepository.java index 1a2e6c2..3bb83ca 100644 --- a/src/main/java/com/server/running_handai/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/server/running_handai/domain/review/repository/ReviewRepository.java @@ -29,4 +29,14 @@ public interface ReviewRepository extends JpaRepository { */ @Query("SELECT AVG(r.stars) FROM Review r WHERE r.course.id = :courseId") Double findAverageStarsByCourseId(@Param("courseId") Long courseId); + + /** + * 특정 회원이 작성한 리뷰 조회 (연관 엔티티 동시 조회) + */ + @Query("SELECT r FROM Review r " + + "LEFT JOIN FETCH r.course c " + + "LEFT JOIN FETCH c.courseImage ci " + + "WHERE r.writer.id = :memberId " + + "ORDER BY r.createdAt DESC") + List findReviewsWithDetailsByMemberId(@Param("memberId") Long memberId); } diff --git a/src/main/java/com/server/running_handai/domain/review/service/ReviewService.java b/src/main/java/com/server/running_handai/domain/review/service/ReviewService.java index 4e056d9..729961e 100644 --- a/src/main/java/com/server/running_handai/domain/review/service/ReviewService.java +++ b/src/main/java/com/server/running_handai/domain/review/service/ReviewService.java @@ -4,6 +4,7 @@ import com.server.running_handai.domain.course.repository.CourseRepository; import com.server.running_handai.domain.member.entity.Member; import com.server.running_handai.domain.member.repository.MemberRepository; +import com.server.running_handai.domain.review.dto.MyReviewInfoDto; import com.server.running_handai.domain.review.dto.ReviewCreateResponseDto; import com.server.running_handai.domain.review.dto.ReviewInfoDto; import com.server.running_handai.domain.review.dto.ReviewInfoListDto; @@ -14,8 +15,10 @@ import com.server.running_handai.domain.review.repository.ReviewRepository; import com.server.running_handai.global.response.ResponseCode; import com.server.running_handai.global.response.exception.BusinessException; +import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -163,4 +166,22 @@ public void deleteReview(Long reviewId, Long memberId) { reviewRepository.delete(review); } + /** + * 회원이 작성한 리뷰를 조회합니다. + * + * @param memberId 요청한 회원의 ID + * @return 내 리뷰 조회용 DTO + */ + public List getMyReviews(Long memberId) { + List reviews = reviewRepository.findReviewsWithDetailsByMemberId(memberId); + + if (reviews.isEmpty()) { + return Collections.emptyList(); + } + + return reviews.stream() + .map(MyReviewInfoDto::from) + .collect(Collectors.toList()); + } + } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d34d0f1..0984ea2 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -91,6 +91,10 @@ external: durunubi: base-url: http://apis.data.go.kr/B551011/Durunubi service-key: ${DURUNUBI_SERVICE_KEY} + spot: + base-url: http://apis.data.go.kr/B551011/KorService2 + service-key: ${SPOT_SERVICE_KEY} + radius: 50000 # [국문 관광정보] 위치기반 관광정보 조회 API 거리 반경 (50000m = 5km) springdoc: default-produces-media-type: application/json diff --git a/src/test/java/com/server/running_handai/domain/review/service/ReviewServiceTest.java b/src/test/java/com/server/running_handai/domain/review/service/ReviewServiceTest.java index 3dbb017..6fca5d6 100644 --- a/src/test/java/com/server/running_handai/domain/review/service/ReviewServiceTest.java +++ b/src/test/java/com/server/running_handai/domain/review/service/ReviewServiceTest.java @@ -8,12 +8,14 @@ import com.server.running_handai.domain.course.entity.Area; import com.server.running_handai.domain.course.entity.Course; +import com.server.running_handai.domain.course.entity.CourseImage; import com.server.running_handai.domain.course.entity.CourseLevel; import com.server.running_handai.domain.course.repository.CourseRepository; import com.server.running_handai.domain.member.entity.Member; import com.server.running_handai.domain.member.entity.Provider; import com.server.running_handai.domain.member.entity.Role; import com.server.running_handai.domain.member.repository.MemberRepository; +import com.server.running_handai.domain.review.dto.MyReviewInfoDto; import com.server.running_handai.domain.review.dto.ReviewCreateResponseDto; import com.server.running_handai.domain.review.dto.ReviewInfoListDto; import com.server.running_handai.domain.review.dto.ReviewCreateRequestDto; @@ -28,6 +30,7 @@ import jakarta.validation.Validator; import jakarta.validation.ValidatorFactory; import java.time.LocalDateTime; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; @@ -91,7 +94,7 @@ void setUp() throws ParseException { course = Course.builder() .name("courseName1") - .distance(10) + .distance(10.5) .duration(120) .level(CourseLevel.MEDIUM) .area(Area.HAEUN_GWANGAN) @@ -106,10 +109,14 @@ void setUp() throws ParseException { .contents(VALID_REVIEW_CONTENTS) .build(); + CourseImage courseImage = CourseImage.builder().imgUrl("img_url").build(); + // 테스트 리뷰에 연관관계 설정 review.setWriter(member); review.setCourse(course); + course.updateCourseImage(courseImage); + ReflectionTestUtils.setField(course, "id", 10L); ReflectionTestUtils.setField(review, "id", 100L); ReflectionTestUtils.setField(member, "id", 50L); ReflectionTestUtils.setField(review, "createdAt", LocalDateTime.now()); @@ -547,4 +554,53 @@ void deleteReview_fail_accessDenied() { } } + + @Nested + @DisplayName("내 리뷰 조회 테스트") + class GetMyReviewTest { + + @Test + @DisplayName("내 리뷰 조회 성공 - 리뷰가 있을 때") + void getMyReview_success_hasReview() { + // given + Long memberId = member.getId(); + List myReviews = List.of(review); + + given(reviewRepository.findReviewsWithDetailsByMemberId(memberId)).willReturn(myReviews); + + // when + List myReviewInfoDtos = reviewService.getMyReviews(memberId); + + // then + MyReviewInfoDto firstDto = myReviewInfoDtos.getFirst(); + assertThat(myReviewInfoDtos.size()).isEqualTo(1); + assertThat(firstDto.reviewId()).isEqualTo(review.getId()); + assertThat(firstDto.courseId()).isEqualTo(course.getId()); + assertThat(firstDto.courseName()).isEqualTo(course.getName()); + assertThat(firstDto.thumbnailUrl()).isEqualTo(course.getCourseImage().getImgUrl()); + assertThat(firstDto.area()).isEqualTo(course.getArea().name()); + assertThat(firstDto.distance()).isEqualTo(course.getDistance()); + assertThat(firstDto.duration()).isEqualTo(course.getDuration()); + assertThat(firstDto.maxElevation()).isEqualTo((int) course.getMaxElevation().doubleValue()); + + verify(reviewRepository).findReviewsWithDetailsByMemberId(memberId); + } + + @Test + @DisplayName("내 리뷰 조회 성공 - 리뷰가 없을 때") + void getMyReview_success_HasNoReview() { + // given + Long memberId = member.getId(); + + given(reviewRepository.findReviewsWithDetailsByMemberId(memberId)).willReturn(Collections.emptyList()); + + // when + List myReviewInfoDtos = reviewService.getMyReviews(memberId); + + // then + assertThat(myReviewInfoDtos).isEmpty(); + + verify(reviewRepository).findReviewsWithDetailsByMemberId(memberId); + } + } } \ No newline at end of file