From 06f77e45259e2b04ccff0b5ed28d875cf8081859 Mon Sep 17 00:00:00 2001 From: Bob Sin Date: Mon, 20 May 2024 21:05:55 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[KAN-63]=20=ED=82=B9=EA=B3=A0=ED=8C=A8?= =?UTF-8?q?=EC=8A=A4=20API=20=EC=A0=9C=EA=B1=B0=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/GetKingoPassController.kt | 32 ------------------- .../presentation/dto/GetKingoPassDto.kt | 11 ------- 2 files changed, 43 deletions(-) delete mode 100644 src/main/kotlin/com/restaurant/be/restaurant/presentation/controller/GetKingoPassController.kt delete mode 100644 src/main/kotlin/com/restaurant/be/restaurant/presentation/dto/GetKingoPassDto.kt diff --git a/src/main/kotlin/com/restaurant/be/restaurant/presentation/controller/GetKingoPassController.kt b/src/main/kotlin/com/restaurant/be/restaurant/presentation/controller/GetKingoPassController.kt deleted file mode 100644 index 6628dad..0000000 --- a/src/main/kotlin/com/restaurant/be/restaurant/presentation/controller/GetKingoPassController.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.restaurant.be.restaurant.presentation.controller - -import com.restaurant.be.common.response.CommonResponse -import com.restaurant.be.restaurant.presentation.dto.GetKingoPassResponse -import io.swagger.annotations.Api -import io.swagger.annotations.ApiOperation -import io.swagger.v3.oas.annotations.media.Content -import io.swagger.v3.oas.annotations.media.Schema -import io.swagger.v3.oas.annotations.responses.ApiResponse -import org.springframework.security.access.prepost.PreAuthorize -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController -import java.security.Principal - -@Api(tags = ["02. Restaurant Info"], description = "음식점 서비스") -@RestController -@RequestMapping("/v1/restaurants/kingo-pass") -class GetKingoPassController { - - @GetMapping - @PreAuthorize("hasRole('USER')") - @ApiOperation(value = "성대 킹고 패스 음식점 리스트 조회 API") - @ApiResponse( - responseCode = "200", - description = "성공", - content = [Content(schema = Schema(implementation = GetKingoPassResponse::class))] - ) - fun getRecentHighReview(principal: Principal): CommonResponse { - return CommonResponse.success() - } -} diff --git a/src/main/kotlin/com/restaurant/be/restaurant/presentation/dto/GetKingoPassDto.kt b/src/main/kotlin/com/restaurant/be/restaurant/presentation/dto/GetKingoPassDto.kt deleted file mode 100644 index 6de7c5d..0000000 --- a/src/main/kotlin/com/restaurant/be/restaurant/presentation/dto/GetKingoPassDto.kt +++ /dev/null @@ -1,11 +0,0 @@ -@file:Suppress("ktlint", "MatchingDeclarationName") - -package com.restaurant.be.restaurant.presentation.dto - -import com.restaurant.be.restaurant.presentation.dto.common.RestaurantDto -import io.swagger.v3.oas.annotations.media.Schema - -data class GetKingoPassResponse( - @Schema(description = "킹고패스 식당 리스트") - val restaurants: List -) From 17dabfad65efe88cfc52b09b619cc75674b4521a Mon Sep 17 00:00:00 2001 From: Taehoon Kim <95288696+goathoon@users.noreply.github.com> Date: Mon, 20 May 2024 21:35:54 +0900 Subject: [PATCH 2/2] [KAN-61-KAN-66-KAN-68-KAN-69-KAN-70-KAN-71] Main Merge PR (#47) --- .../be/common/config/SwaggerConfig.kt | 14 + .../be/common/exception/ServerException.kt | 24 + .../be/review/domain/entity/Review.kt | 48 +- .../be/review/domain/entity/ReviewLikes.kt | 1 + .../domain/service/DeleteReviewService.kt | 27 ++ .../review/domain/service/GetReviewService.kt | 69 ++- .../domain/service/LikeReviewService.kt | 71 +++ .../domain/service/UpdateReviewService.kt | 44 ++ .../controller/DeleteReviewController.kt | 11 +- .../controller/GetMyReviewController.kt | 18 +- .../controller/GetReviewController.kt | 27 +- .../controller/LikeReviewController.kt | 13 +- .../controller/UpdateReviewController.kt | 15 +- .../review/presentation/dto/GetMyReviewDto.kt | 4 +- .../review/presentation/dto/GetReviewDto.kt | 7 +- .../review/presentation/dto/LikeReviewDto.kt | 10 +- .../presentation/dto/common/ReviewDto.kt | 27 +- .../repository/ReviewLikesRepository.kt | 1 + .../repository/ReviewRepositoryCustom.kt | 11 + .../repository/ReviewRepositoryCustomImpl.kt | 79 ++++ .../be/review/ReviewIntegrationTest.kt | 435 +++++++++++++++++- 21 files changed, 881 insertions(+), 75 deletions(-) create mode 100644 src/main/kotlin/com/restaurant/be/review/domain/service/DeleteReviewService.kt create mode 100644 src/main/kotlin/com/restaurant/be/review/domain/service/LikeReviewService.kt create mode 100644 src/main/kotlin/com/restaurant/be/review/domain/service/UpdateReviewService.kt diff --git a/src/main/kotlin/com/restaurant/be/common/config/SwaggerConfig.kt b/src/main/kotlin/com/restaurant/be/common/config/SwaggerConfig.kt index d969dbf..b89a331 100644 --- a/src/main/kotlin/com/restaurant/be/common/config/SwaggerConfig.kt +++ b/src/main/kotlin/com/restaurant/be/common/config/SwaggerConfig.kt @@ -1,5 +1,7 @@ package com.restaurant.be.common.config +import io.swagger.annotations.ApiModel +import io.swagger.annotations.ApiModelProperty import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.http.HttpMethod @@ -76,4 +78,16 @@ class SwaggerConfig { return ApiInfoBuilder().title("REST API Document.") .description("work in progress").termsOfServiceUrl("localhost").version("1.0").build() } + + @ApiModel + class PageModel { + @ApiModelProperty(value = "페이지 번호(0..N)", example = "0") + val page: Int = 0 + + @ApiModelProperty(value = "페이지 크기", allowableValues = "range[0, 100]", example = "0") + val size: Int = 0 + + @ApiModelProperty(value = "정렬(사용법: 컬럼명,ASC|DESC)") + val sort: List = listOf() + } } diff --git a/src/main/kotlin/com/restaurant/be/common/exception/ServerException.kt b/src/main/kotlin/com/restaurant/be/common/exception/ServerException.kt index f8cd991..2c87928 100644 --- a/src/main/kotlin/com/restaurant/be/common/exception/ServerException.kt +++ b/src/main/kotlin/com/restaurant/be/common/exception/ServerException.kt @@ -17,6 +17,10 @@ data class NotFoundUserEmailException( override val message: String = "존재 하지 않는 유저 이메일 입니다." ) : ServerException(400, message) +data class NotFoundUserIdException( + override val message: String = "유저 고유 ID가 존재하지 않습니다" +) : ServerException(500, message) + data class WithdrawalUserException( override val message: String = "탈퇴한 유저 입니다." ) : ServerException(400, message) @@ -56,3 +60,23 @@ data class NotFoundUserException( data class NotFoundReviewException( override val message: String = "존재하지 않은 리뷰 입니다." ) : ServerException(400, message) + +data class UnAuthorizedUpdateException( + override val message: String = "해당 게시글을 수정할 권한이 없습니다." +) : ServerException(401, message) + +data class UnAuthorizedDeleteException( + override val message: String = "해당 게시글을 삭제할 권한이 없습니다." +) : ServerException(401, message) + +data class DuplicateLikeException( + override val message: String = "같은 게시글을 두번 좋아요할 수 없습니다." +) : ServerException(400, message) + +data class NotFoundLikeException( + override val message: String = "해당 게시글은 이미 좋아하지 않습니다." +) : ServerException(400, message) + +data class InvalidLikeCountException( + override val message: String = "좋아요가 0보다 작아질 순 없습니다." +) : ServerException(500, message) diff --git a/src/main/kotlin/com/restaurant/be/review/domain/entity/Review.kt b/src/main/kotlin/com/restaurant/be/review/domain/entity/Review.kt index 30f3064..77cb137 100644 --- a/src/main/kotlin/com/restaurant/be/review/domain/entity/Review.kt +++ b/src/main/kotlin/com/restaurant/be/review/domain/entity/Review.kt @@ -1,7 +1,11 @@ package com.restaurant.be.review.domain.entity import com.restaurant.be.common.entity.BaseEntity +import com.restaurant.be.common.exception.InvalidLikeCountException +import com.restaurant.be.review.domain.entity.QReview.review +import com.restaurant.be.review.presentation.dto.UpdateReviewRequest import com.restaurant.be.review.presentation.dto.common.ReviewResponseDto +import com.restaurant.be.user.domain.entity.QUser.user import com.restaurant.be.user.domain.entity.User import kotlinx.serialization.json.JsonNull.content import javax.persistence.CascadeType @@ -32,23 +36,57 @@ class Review( val restaurantId: Long, @Column(nullable = false) - val content: String, + var content: String, @Column(nullable = false) - val rating: Double, + var rating: Double, + + @Column(name = "like_count", nullable = false) + var likeCount: Long = 0, + + @Column(name = "view_count", nullable = false) + var viewCount: Long = 0, // 부모 (Review Entity)가 주인이되어 Image참조 가능. 반대는 불가능 @OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true) @JoinColumn(name = "review_id") - val images: MutableList = mutableListOf() + var images: MutableList = mutableListOf() ) : BaseEntity() { fun addImage(reviewImage: ReviewImage) { images.add(reviewImage) } + fun updateReview(request: UpdateReviewRequest) { + val updateRequest = request.review + this.content = updateRequest.content + this.rating = updateRequest.rating + this.images.clear() + updateRequest.imageUrls.forEach { + this.addImage( + ReviewImage( + imageUrl = it + ) + ) + } + } + + fun incrementViewCount() { + this.viewCount++ + } + fun incrementLikeCount() { + this.likeCount++ + } + fun decrementLikeCount() { + if (this.likeCount == 0L) { + throw InvalidLikeCountException() + } + this.likeCount-- + } + fun toResponseDTO(doesUserLike: Boolean): ReviewResponseDto { return ReviewResponseDto( + id = id ?: 0, userId = user.id ?: 0, username = user.nickname, profileImageUrl = user.profileImageUrl, @@ -56,7 +94,9 @@ class Review( rating = rating, content = content, imageUrls = images.map { it.imageUrl }, - isLike = doesUserLike + isLike = doesUserLike, + viewCount = viewCount, + likeCount = likeCount ) } } diff --git a/src/main/kotlin/com/restaurant/be/review/domain/entity/ReviewLikes.kt b/src/main/kotlin/com/restaurant/be/review/domain/entity/ReviewLikes.kt index 1b015c7..97f0de3 100644 --- a/src/main/kotlin/com/restaurant/be/review/domain/entity/ReviewLikes.kt +++ b/src/main/kotlin/com/restaurant/be/review/domain/entity/ReviewLikes.kt @@ -1,3 +1,4 @@ + package com.restaurant.be.review.domain.entity import javax.persistence.Column diff --git a/src/main/kotlin/com/restaurant/be/review/domain/service/DeleteReviewService.kt b/src/main/kotlin/com/restaurant/be/review/domain/service/DeleteReviewService.kt new file mode 100644 index 0000000..bd2dccf --- /dev/null +++ b/src/main/kotlin/com/restaurant/be/review/domain/service/DeleteReviewService.kt @@ -0,0 +1,27 @@ +package com.restaurant.be.review.domain.service + +import com.restaurant.be.common.exception.NotFoundReviewException +import com.restaurant.be.common.exception.NotFoundUserEmailException +import com.restaurant.be.common.exception.UnAuthorizedDeleteException +import com.restaurant.be.review.repository.ReviewRepository +import com.restaurant.be.user.repository.UserRepository +import org.springframework.stereotype.Service +import javax.transaction.Transactional +import kotlin.jvm.optionals.getOrNull + +@Service +class DeleteReviewService( + private val reviewRepository: ReviewRepository, + private val userRepository: UserRepository +) { + @Transactional + fun deleteReview(reviewId: Long, email: String) { + val user = userRepository.findByEmail(email) ?: throw NotFoundUserEmailException() + + var review = reviewRepository.findById(reviewId).getOrNull() ?: throw NotFoundReviewException() + + if (user.id != review.user.id) throw UnAuthorizedDeleteException() + + reviewRepository.deleteById(reviewId) + } +} diff --git a/src/main/kotlin/com/restaurant/be/review/domain/service/GetReviewService.kt b/src/main/kotlin/com/restaurant/be/review/domain/service/GetReviewService.kt index c6aebca..97a9319 100644 --- a/src/main/kotlin/com/restaurant/be/review/domain/service/GetReviewService.kt +++ b/src/main/kotlin/com/restaurant/be/review/domain/service/GetReviewService.kt @@ -1,38 +1,75 @@ package com.restaurant.be.review.domain.service +import com.restaurant.be.common.exception.NotFoundReviewException import com.restaurant.be.common.exception.NotFoundUserEmailException +import com.restaurant.be.review.presentation.dto.GetMyReviewsResponse import com.restaurant.be.review.presentation.dto.GetReviewResponse -import com.restaurant.be.review.repository.ReviewLikesRepository +import com.restaurant.be.review.presentation.dto.GetReviewsResponse +import com.restaurant.be.review.presentation.dto.ReviewWithLikesDto +import com.restaurant.be.review.presentation.dto.common.ReviewResponseDto import com.restaurant.be.review.repository.ReviewRepository import com.restaurant.be.user.repository.UserRepository -import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional @Service class GetReviewService( private val userRepository: UserRepository, - private val reviewRepository: ReviewRepository, - private val reviewLikesRepository: ReviewLikesRepository + private val reviewRepository: ReviewRepository + ) { - fun getReviewListOf(page: Int, size: Int, email: String): GetReviewResponse { - val pageable = PageRequest.of(page, size) - val reviews = reviewRepository.findAll(pageable).content + @Transactional(readOnly = true) + fun getReviews(pageable: Pageable, email: String): GetReviewsResponse { + val user = userRepository.findByEmail(email) + ?: throw NotFoundUserEmailException() + + val reviewsWithLikes = reviewRepository.findReviews(user, pageable) + + val responseDtos = convertResponeDto(reviewsWithLikes) + return GetReviewsResponse(responseDtos) + } + + @Transactional + fun getReview(reviewId: Long, email: String): GetReviewResponse { val user = userRepository.findByEmail(email) ?: throw NotFoundUserEmailException() - return GetReviewResponse( - reviews.map { - it - .toResponseDTO(doesUserLike = isReviewLikedByUser(user.id, it.id)) - } + val reviewWithLikes = reviewRepository.findReview(user, reviewId) + ?: throw NotFoundReviewException() + + if (reviewWithLikes.review.user.id != user.id) { + reviewWithLikes.review.incrementViewCount() + } + + val responseDto = ReviewResponseDto.toDto( + reviewWithLikes.review, + reviewWithLikes.isLikedByUser ) + + return GetReviewResponse(responseDto) + } + + @Transactional(readOnly = true) + fun getMyReviews(pageable: Pageable, email: String): GetMyReviewsResponse { + val user = userRepository.findByEmail(email) + ?: throw NotFoundUserEmailException() + + val reviewsWithLikes = reviewRepository.findMyReviews(user, pageable) + + val reviewResponses = convertResponeDto(reviewsWithLikes) + + return GetMyReviewsResponse(reviewResponses) } - fun isReviewLikedByUser(userId: Long?, reviewId: Long?): Boolean { - if (userId != 0L) { - return reviewLikesRepository.existsByReviewIdAndUserId(userId, reviewId) + private fun convertResponeDto(reviewsWithLikes: List): List { + val reviewResponses = reviewsWithLikes.map { + ReviewResponseDto.toDto( + it.review, + it.isLikedByUser + ) } - return false + return reviewResponses } } diff --git a/src/main/kotlin/com/restaurant/be/review/domain/service/LikeReviewService.kt b/src/main/kotlin/com/restaurant/be/review/domain/service/LikeReviewService.kt new file mode 100644 index 0000000..33b8e3d --- /dev/null +++ b/src/main/kotlin/com/restaurant/be/review/domain/service/LikeReviewService.kt @@ -0,0 +1,71 @@ +package com.restaurant.be.review.domain.service + +import com.restaurant.be.common.exception.DuplicateLikeException +import com.restaurant.be.common.exception.NotFoundLikeException +import com.restaurant.be.common.exception.NotFoundReviewException +import com.restaurant.be.common.exception.NotFoundUserEmailException +import com.restaurant.be.common.exception.NotFoundUserIdException +import com.restaurant.be.review.domain.entity.QReview.review +import com.restaurant.be.review.presentation.dto.LikeReviewRequest +import com.restaurant.be.review.presentation.dto.LikeReviewResponse +import com.restaurant.be.review.presentation.dto.ReviewWithLikesDto +import com.restaurant.be.review.presentation.dto.common.ReviewResponseDto +import com.restaurant.be.review.repository.ReviewLikesRepository +import com.restaurant.be.review.repository.ReviewRepository +import com.restaurant.be.user.domain.entity.User +import com.restaurant.be.user.repository.UserRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class LikeReviewService( + val userRepository: UserRepository, + val reviewLikesRepository: ReviewLikesRepository, + val reviewRepository: ReviewRepository +) { + @Transactional + fun likeReview(reviewId: Long, request: LikeReviewRequest, email: String): LikeReviewResponse { + val user = userRepository.findByEmail(email) + ?: throw NotFoundUserEmailException() + + val userId = user.id ?: throw NotFoundUserIdException() + + likeReviewWhetherAlreadyLikeOrNot(request, reviewId, user, userId) + + val reviewWithLikes: ReviewWithLikesDto? = reviewRepository.findReview(user, reviewId) + ?: throw NotFoundReviewException() + + val responseDto = ReviewResponseDto.toDto( + reviewWithLikes!!.review, + reviewWithLikes.isLikedByUser + ) + + return LikeReviewResponse(responseDto) + } + + private fun likeReviewWhetherAlreadyLikeOrNot( + request: LikeReviewRequest, + reviewId: Long, + user: User, + userId: Long + ) { + if (request.isLike) { + if (isAlreadyLike(reviewId, user)) { + throw DuplicateLikeException() + } + reviewLikesRepository.save(request.toEntity(userId, reviewId)) + val review = reviewRepository.findById(reviewId) + review.get().incrementLikeCount() + } else { + if (!isAlreadyLike(reviewId, user)) { + throw NotFoundLikeException() + } + reviewLikesRepository.deleteByReviewIdAndUserId(reviewId, userId) + val review = reviewRepository.findById(reviewId) + review.get().decrementLikeCount() + } + } + + private fun isAlreadyLike(reviewId: Long, user: User) = + reviewLikesRepository.existsByReviewIdAndUserId(reviewId, user.id) +} diff --git a/src/main/kotlin/com/restaurant/be/review/domain/service/UpdateReviewService.kt b/src/main/kotlin/com/restaurant/be/review/domain/service/UpdateReviewService.kt new file mode 100644 index 0000000..b662b25 --- /dev/null +++ b/src/main/kotlin/com/restaurant/be/review/domain/service/UpdateReviewService.kt @@ -0,0 +1,44 @@ +package com.restaurant.be.review.domain.service + +import com.restaurant.be.common.exception.NotFoundReviewException +import com.restaurant.be.common.exception.NotFoundUserEmailException +import com.restaurant.be.common.exception.UnAuthorizedUpdateException +import com.restaurant.be.review.domain.entity.QReview.review +import com.restaurant.be.review.presentation.dto.UpdateReviewRequest +import com.restaurant.be.review.presentation.dto.UpdateReviewResponse +import com.restaurant.be.review.presentation.dto.common.ReviewResponseDto +import com.restaurant.be.review.repository.ReviewRepository +import com.restaurant.be.user.repository.UserRepository +import org.springframework.stereotype.Service +import javax.transaction.Transactional +import kotlin.jvm.optionals.getOrNull + +@Service +class UpdateReviewService( + private val reviewRepository: ReviewRepository, + private val userRepository: UserRepository +) { + @Transactional + fun updateReview(restaurantId: Long, reviewId: Long, reviewRequest: UpdateReviewRequest, email: String): UpdateReviewResponse { + val user = userRepository.findByEmail(email) + ?: throw NotFoundUserEmailException() + + val review = reviewRepository.findById(reviewId) + .getOrNull() + ?: throw NotFoundReviewException() + + if (user.id != review.user.id) throw UnAuthorizedUpdateException() + + review.updateReview(reviewRequest) + + val reviewWithLikes = reviewRepository.findReview(user, reviewId) + ?: throw NotFoundReviewException() + + val responseDto = ReviewResponseDto.toDto( + reviewWithLikes.review, + reviewWithLikes.isLikedByUser + ) + + return UpdateReviewResponse(responseDto) + } +} diff --git a/src/main/kotlin/com/restaurant/be/review/presentation/controller/DeleteReviewController.kt b/src/main/kotlin/com/restaurant/be/review/presentation/controller/DeleteReviewController.kt index 44d8c1a..5604a4e 100644 --- a/src/main/kotlin/com/restaurant/be/review/presentation/controller/DeleteReviewController.kt +++ b/src/main/kotlin/com/restaurant/be/review/presentation/controller/DeleteReviewController.kt @@ -1,6 +1,7 @@ package com.restaurant.be.review.presentation.controller import com.restaurant.be.common.response.CommonResponse +import com.restaurant.be.review.domain.service.DeleteReviewService import io.swagger.annotations.Api import io.swagger.annotations.ApiOperation import io.swagger.v3.oas.annotations.responses.ApiResponse @@ -14,9 +15,11 @@ import java.security.Principal @Api(tags = ["03. Review Info"], description = "리뷰 서비스") @RestController @RequestMapping("/v1/restaurants/") -class DeleteReviewController { +class DeleteReviewController( + val deleteReviewService: DeleteReviewService +) { - @DeleteMapping("/{restaurantId}/reviews/{reviewId}") + @DeleteMapping("/reviews/{reviewId}") @PreAuthorize("hasRole('USER')") @ApiOperation(value = "리뷰 삭제 API") @ApiResponse( @@ -25,9 +28,9 @@ class DeleteReviewController { ) fun deleteReview( principal: Principal, - @PathVariable restaurantId: String, - @PathVariable reviewId: String + @PathVariable reviewId: Long ): CommonResponse { + deleteReviewService.deleteReview(reviewId, principal.name) return CommonResponse.success() } } diff --git a/src/main/kotlin/com/restaurant/be/review/presentation/controller/GetMyReviewController.kt b/src/main/kotlin/com/restaurant/be/review/presentation/controller/GetMyReviewController.kt index 100ba75..521550e 100644 --- a/src/main/kotlin/com/restaurant/be/review/presentation/controller/GetMyReviewController.kt +++ b/src/main/kotlin/com/restaurant/be/review/presentation/controller/GetMyReviewController.kt @@ -1,12 +1,14 @@ package com.restaurant.be.review.presentation.controller import com.restaurant.be.common.response.CommonResponse -import com.restaurant.be.review.presentation.dto.GetMyReviewResponse +import com.restaurant.be.review.domain.service.GetReviewService +import com.restaurant.be.review.presentation.dto.GetMyReviewsResponse import io.swagger.annotations.Api import io.swagger.annotations.ApiOperation import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse +import org.springframework.data.domain.Pageable import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping @@ -16,7 +18,9 @@ import java.security.Principal @Api(tags = ["03. Review Info"], description = "리뷰 서비스") @RestController @RequestMapping("/v1/restaurants/my-reviews") -class GetMyReviewController { +class GetMyReviewController( + val getReviewService: GetReviewService +) { @GetMapping @PreAuthorize("hasRole('USER')") @@ -24,11 +28,13 @@ class GetMyReviewController { @ApiResponse( responseCode = "200", description = "성공", - content = [Content(schema = Schema(implementation = GetMyReviewResponse::class))] + content = [Content(schema = Schema(implementation = GetMyReviewsResponse::class))] ) fun getMyReview( - principal: Principal - ): CommonResponse { - return CommonResponse.success() + principal: Principal, + pageable: Pageable + ): CommonResponse { + val response = getReviewService.getMyReviews(pageable, principal.name) + return CommonResponse.success(response) } } diff --git a/src/main/kotlin/com/restaurant/be/review/presentation/controller/GetReviewController.kt b/src/main/kotlin/com/restaurant/be/review/presentation/controller/GetReviewController.kt index b2e4950..90f63f2 100644 --- a/src/main/kotlin/com/restaurant/be/review/presentation/controller/GetReviewController.kt +++ b/src/main/kotlin/com/restaurant/be/review/presentation/controller/GetReviewController.kt @@ -3,15 +3,17 @@ package com.restaurant.be.review.presentation.controller import com.restaurant.be.common.response.CommonResponse import com.restaurant.be.review.domain.service.GetReviewService import com.restaurant.be.review.presentation.dto.GetReviewResponse +import com.restaurant.be.review.presentation.dto.GetReviewsResponse import io.swagger.annotations.Api import io.swagger.annotations.ApiOperation import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse +import org.springframework.data.domain.Pageable import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import java.security.Principal @@ -30,12 +32,27 @@ class GetReviewController( description = "성공", content = [Content(schema = Schema(implementation = GetReviewResponse::class))] ) - fun getReview( + fun getReviews( principal: Principal, - @RequestParam page: Int, - size: Int + pageable: Pageable + ): CommonResponse { + val response = getReviewService.getReviews(pageable, principal.name) + return CommonResponse.success(response) + } + + @GetMapping("/{reviewId}") + @PreAuthorize("hasRole('USER')") + @ApiOperation(value = "리뷰 단건 조회 API") + @ApiResponse( + responseCode = "200", + description = "성공", + content = [Content(schema = Schema(implementation = GetReviewResponse::class))] + ) + fun getOneReview( + principal: Principal, + @PathVariable reviewId: Long ): CommonResponse { - val response = getReviewService.getReviewListOf(page, size, principal.name) + val response = getReviewService.getReview(reviewId, principal.name) return CommonResponse.success(response) } } diff --git a/src/main/kotlin/com/restaurant/be/review/presentation/controller/LikeReviewController.kt b/src/main/kotlin/com/restaurant/be/review/presentation/controller/LikeReviewController.kt index fd33b4d..8521b8e 100644 --- a/src/main/kotlin/com/restaurant/be/review/presentation/controller/LikeReviewController.kt +++ b/src/main/kotlin/com/restaurant/be/review/presentation/controller/LikeReviewController.kt @@ -1,6 +1,7 @@ package com.restaurant.be.review.presentation.controller import com.restaurant.be.common.response.CommonResponse +import com.restaurant.be.review.domain.service.LikeReviewService import com.restaurant.be.review.presentation.dto.LikeReviewRequest import com.restaurant.be.review.presentation.dto.LikeReviewResponse import io.swagger.annotations.Api @@ -20,9 +21,11 @@ import javax.validation.Valid @Api(tags = ["03. Review Info"], description = "리뷰 서비스") @RestController @RequestMapping("/v1/restaurants") -class LikeReviewController { +class LikeReviewController( + val likeReviewService: LikeReviewService +) { - @PostMapping("/{restaurantId}/reviews/{reviewId}/like") + @PostMapping("/reviews/{reviewId}/like") @PreAuthorize("hasRole('USER')") @ApiOperation(value = "리뷰 좋아요 API") @ApiResponse( @@ -32,11 +35,11 @@ class LikeReviewController { ) fun likeReview( principal: Principal, - @PathVariable restaurantId: String, - @PathVariable reviewId: String, + @PathVariable reviewId: Long, @RequestBody @Valid request: LikeReviewRequest ): CommonResponse { - return CommonResponse.success() + val response = likeReviewService.likeReview(reviewId, request, principal.name) + return CommonResponse.success(response) } } diff --git a/src/main/kotlin/com/restaurant/be/review/presentation/controller/UpdateReviewController.kt b/src/main/kotlin/com/restaurant/be/review/presentation/controller/UpdateReviewController.kt index fe8e102..4a4622c 100644 --- a/src/main/kotlin/com/restaurant/be/review/presentation/controller/UpdateReviewController.kt +++ b/src/main/kotlin/com/restaurant/be/review/presentation/controller/UpdateReviewController.kt @@ -1,6 +1,8 @@ package com.restaurant.be.review.presentation.controller import com.restaurant.be.common.response.CommonResponse +import com.restaurant.be.review.domain.service.UpdateReviewService +import com.restaurant.be.review.presentation.dto.UpdateReviewRequest import com.restaurant.be.review.presentation.dto.UpdateReviewResponse import io.swagger.annotations.Api import io.swagger.annotations.ApiOperation @@ -19,7 +21,9 @@ import javax.validation.Valid @Api(tags = ["03. Review Info"], description = "리뷰 서비스") @RestController @RequestMapping("/v1/restaurants/reviews") -class UpdateReviewController { +class UpdateReviewController( + val updateReviewService: UpdateReviewService +) { @PatchMapping("/{restaurantId}/reviews/{reviewId}") @PreAuthorize("hasRole('USER')") @@ -31,11 +35,12 @@ class UpdateReviewController { ) fun updateReview( principal: Principal, - @PathVariable restaurantId: String, - @PathVariable reviewId: String, + @PathVariable restaurantId: Long, + @PathVariable reviewId: Long, @RequestBody @Valid - request: UpdateReviewResponse + request: UpdateReviewRequest ): CommonResponse { - return CommonResponse.success() + val response = updateReviewService.updateReview(restaurantId, reviewId, request, principal.name) + return CommonResponse.success(response) } } diff --git a/src/main/kotlin/com/restaurant/be/review/presentation/dto/GetMyReviewDto.kt b/src/main/kotlin/com/restaurant/be/review/presentation/dto/GetMyReviewDto.kt index 30dd251..6b80edb 100644 --- a/src/main/kotlin/com/restaurant/be/review/presentation/dto/GetMyReviewDto.kt +++ b/src/main/kotlin/com/restaurant/be/review/presentation/dto/GetMyReviewDto.kt @@ -5,7 +5,7 @@ package com.restaurant.be.review.presentation.dto import com.restaurant.be.review.presentation.dto.common.ReviewResponseDto import io.swagger.v3.oas.annotations.media.Schema -data class GetMyReviewResponse( - @Schema(description = "리뷰 리스트") +data class GetMyReviewsResponse( + @Schema(description = "내가 작성한 리뷰 리스트") val reviews: List ) diff --git a/src/main/kotlin/com/restaurant/be/review/presentation/dto/GetReviewDto.kt b/src/main/kotlin/com/restaurant/be/review/presentation/dto/GetReviewDto.kt index d2cf35c..b957b59 100644 --- a/src/main/kotlin/com/restaurant/be/review/presentation/dto/GetReviewDto.kt +++ b/src/main/kotlin/com/restaurant/be/review/presentation/dto/GetReviewDto.kt @@ -5,7 +5,12 @@ package com.restaurant.be.review.presentation.dto import com.restaurant.be.review.presentation.dto.common.ReviewResponseDto import io.swagger.v3.oas.annotations.media.Schema -data class GetReviewResponse( +data class GetReviewsResponse( @Schema(description = "리뷰 리스트") val reviews: List ) + +data class GetReviewResponse( + @Schema(description = "리뷰 단건") + val review: ReviewResponseDto +) diff --git a/src/main/kotlin/com/restaurant/be/review/presentation/dto/LikeReviewDto.kt b/src/main/kotlin/com/restaurant/be/review/presentation/dto/LikeReviewDto.kt index 8c8ffad..6aecc5d 100644 --- a/src/main/kotlin/com/restaurant/be/review/presentation/dto/LikeReviewDto.kt +++ b/src/main/kotlin/com/restaurant/be/review/presentation/dto/LikeReviewDto.kt @@ -1,5 +1,6 @@ package com.restaurant.be.review.presentation.dto +import com.restaurant.be.review.domain.entity.ReviewLikes import com.restaurant.be.review.presentation.dto.common.ReviewResponseDto import io.swagger.annotations.ApiModelProperty import io.swagger.v3.oas.annotations.media.Schema @@ -7,7 +8,14 @@ import io.swagger.v3.oas.annotations.media.Schema data class LikeReviewRequest( @ApiModelProperty(value = "리뷰 좋아요 여부", required = true) val isLike: Boolean -) +) { + fun toEntity(userId: Long, reviewId: Long): ReviewLikes { + return ReviewLikes( + userId = userId, + reviewId = reviewId + ) + } +} data class LikeReviewResponse( @Schema(description = "리뷰 정보") diff --git a/src/main/kotlin/com/restaurant/be/review/presentation/dto/common/ReviewDto.kt b/src/main/kotlin/com/restaurant/be/review/presentation/dto/common/ReviewDto.kt index 8910ec5..03206b0 100644 --- a/src/main/kotlin/com/restaurant/be/review/presentation/dto/common/ReviewDto.kt +++ b/src/main/kotlin/com/restaurant/be/review/presentation/dto/common/ReviewDto.kt @@ -30,6 +30,8 @@ data class ReviewRequestDto( } data class ReviewResponseDto( + @Schema(description = "리뷰 id") + val id: Long, @Schema(description = "유저 id") val userId: Long, @Schema(description = "유저 닉네임") @@ -45,19 +47,26 @@ data class ReviewResponseDto( @Schema(description = "이미지 url 리스트") val imageUrls: List, @Schema(description = "좋아요 여부") - val isLike: Boolean + val isLike: Boolean, + @Schema(description = "좋아요 받은 횟수") + val likeCount: Long, + @Schema(description = "리뷰 조회 수") + val viewCount: Long ) { companion object { fun toDto(review: Review, isLikedByUser: Boolean? = null): ReviewResponseDto { return ReviewResponseDto( - review.user.id ?: 0, - review.user.nickname, - review.user.profileImageUrl, - review.restaurantId, - review.rating, - review.content, - review.images.map { it.imageUrl }, - isLikedByUser ?: false + id = review.id ?: 0, + userId = review.user.id ?: 0, + username = review.user.nickname, + profileImageUrl = review.user.profileImageUrl, + restaurantId = review.restaurantId, + rating = review.rating, + content = review.content, + imageUrls = review.images.map { it.imageUrl }, + isLike = isLikedByUser ?: false, + likeCount = review.likeCount, + viewCount = review.viewCount ) } } diff --git a/src/main/kotlin/com/restaurant/be/review/repository/ReviewLikesRepository.kt b/src/main/kotlin/com/restaurant/be/review/repository/ReviewLikesRepository.kt index 39b71cf..9525711 100644 --- a/src/main/kotlin/com/restaurant/be/review/repository/ReviewLikesRepository.kt +++ b/src/main/kotlin/com/restaurant/be/review/repository/ReviewLikesRepository.kt @@ -7,4 +7,5 @@ import org.springframework.stereotype.Repository @Repository interface ReviewLikesRepository : JpaRepository { fun existsByReviewIdAndUserId(reviewId: Long?, userId: Long?): Boolean + fun deleteByReviewIdAndUserId(reviewId: Long?, userId: Long?) } diff --git a/src/main/kotlin/com/restaurant/be/review/repository/ReviewRepositoryCustom.kt b/src/main/kotlin/com/restaurant/be/review/repository/ReviewRepositoryCustom.kt index 5534ea3..b2235fd 100644 --- a/src/main/kotlin/com/restaurant/be/review/repository/ReviewRepositoryCustom.kt +++ b/src/main/kotlin/com/restaurant/be/review/repository/ReviewRepositoryCustom.kt @@ -2,10 +2,21 @@ package com.restaurant.be.review.repository import com.restaurant.be.review.presentation.dto.ReviewWithLikesDto import com.restaurant.be.user.domain.entity.User +import org.springframework.data.domain.Pageable interface ReviewRepositoryCustom { fun findReview( user: User, reviewId: Long ): ReviewWithLikesDto? + + fun findReviews( + user: User, + pageable: Pageable + ): List + + fun findMyReviews( + user: User, + pageable: Pageable + ): List } diff --git a/src/main/kotlin/com/restaurant/be/review/repository/ReviewRepositoryCustomImpl.kt b/src/main/kotlin/com/restaurant/be/review/repository/ReviewRepositoryCustomImpl.kt index 9818f70..d22de5d 100644 --- a/src/main/kotlin/com/restaurant/be/review/repository/ReviewRepositoryCustomImpl.kt +++ b/src/main/kotlin/com/restaurant/be/review/repository/ReviewRepositoryCustomImpl.kt @@ -1,11 +1,16 @@ package com.restaurant.be.review.repository +import com.querydsl.core.types.OrderSpecifier import com.querydsl.core.types.Projections +import com.querydsl.core.types.dsl.PathBuilder +import com.querydsl.core.types.dsl.PathBuilderFactory import com.querydsl.jpa.impl.JPAQueryFactory import com.restaurant.be.review.domain.entity.QReview.review import com.restaurant.be.review.domain.entity.QReviewLikes.reviewLikes +import com.restaurant.be.review.domain.entity.Review import com.restaurant.be.review.presentation.dto.ReviewWithLikesDto import com.restaurant.be.user.domain.entity.User +import org.springframework.data.domain.Pageable class ReviewRepositoryCustomImpl( private val queryFactory: JPAQueryFactory @@ -32,4 +37,78 @@ class ReviewRepositoryCustomImpl( .fetchJoin() .fetchOne() } + + override fun findReviews(user: User, pageable: Pageable): List { + val orderSpecifier = setOrderSpecifier(pageable) + + val reviewsWithLikes = queryFactory + .select( + Projections.constructor( + ReviewWithLikesDto::class.java, + review, + reviewLikes.userId.isNotNull() + ) + ) + .from(review) + .leftJoin(reviewLikes) + .on( + reviewLikes.reviewId.eq(review.id) + .and(reviewLikes.userId.eq(user.id)) + ) + .offset(pageable.offset) + .limit(pageable.pageSize.toLong()) + .orderBy(*orderSpecifier.toTypedArray()) + .fetchJoin() + .fetch() + + return reviewsWithLikes + } + + override fun findMyReviews(user: User, pageable: Pageable): List { + val orderSpecifier = setOrderSpecifier(pageable) + + val reviewsWithLikes = queryFactory + .select( + Projections.constructor( + ReviewWithLikesDto::class.java, + review, + reviewLikes.userId.isNotNull() + ) + ) + .from(review) + .leftJoin(reviewLikes) + .on( + reviewLikes.reviewId.eq(review.id) + .and(reviewLikes.userId.eq(user.id)) + ) + .where(review.user.id.eq(user.id)) + .offset(pageable.offset) + .limit(pageable.pageSize.toLong()) + .orderBy(*orderSpecifier.toTypedArray()) + .fetchJoin() + .fetch() + + return reviewsWithLikes + } + + private fun setOrderSpecifier(pageable: Pageable): List> { + val pathBuilder: PathBuilder = PathBuilderFactory().create(Review::class.java) + val sort = pageable.sort + + val orderSpecifiers: MutableList> = mutableListOf() + + for (order in sort) { + val property = order.property + val direction = order.direction + + val orderSpecifier: OrderSpecifier<*> = if (direction.isAscending) { + pathBuilder.getString(property).asc() + } else { + pathBuilder.getString(property).desc() + } + + orderSpecifiers.add(orderSpecifier) + } + return orderSpecifiers + } } diff --git a/src/test/kotlin/com/restaurant/be/review/ReviewIntegrationTest.kt b/src/test/kotlin/com/restaurant/be/review/ReviewIntegrationTest.kt index 3a4a58b..0443eaa 100644 --- a/src/test/kotlin/com/restaurant/be/review/ReviewIntegrationTest.kt +++ b/src/test/kotlin/com/restaurant/be/review/ReviewIntegrationTest.kt @@ -6,9 +6,16 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import com.restaurant.be.common.CustomDescribeSpec import com.restaurant.be.common.IntegrationTest +import com.restaurant.be.common.exception.DuplicateLikeException +import com.restaurant.be.common.exception.NotFoundReviewException import com.restaurant.be.common.response.CommonResponse import com.restaurant.be.review.domain.entity.Review import com.restaurant.be.review.presentation.dto.CreateReviewResponse +import com.restaurant.be.review.presentation.dto.GetReviewResponse +import com.restaurant.be.review.presentation.dto.LikeReviewRequest +import com.restaurant.be.review.presentation.dto.LikeReviewResponse +import com.restaurant.be.review.presentation.dto.UpdateReviewRequest +import com.restaurant.be.review.presentation.dto.UpdateReviewResponse import com.restaurant.be.review.presentation.dto.common.ReviewRequestDto import com.restaurant.be.review.repository.ReviewRepository import com.restaurant.be.user.domain.entity.QUser.user @@ -27,22 +34,16 @@ import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status -import org.testcontainers.shaded.org.bouncycastle.cms.RecipientId.password import java.nio.charset.StandardCharsets import javax.transaction.Transactional @IntegrationTest class ReviewIntegrationTest( - @Autowired - private val mockMvc: MockMvc, - @Autowired - private val objectMapper: ObjectMapper, - @Autowired - private val signUpUserService: SignUpUserService, - @Autowired - private val signUpUserRepository: UserRepository, - @Autowired - private val reviewRepository: ReviewRepository + @Autowired private val mockMvc: MockMvc, + @Autowired private val objectMapper: ObjectMapper, + @Autowired private val signUpUserService: SignUpUserService, + @Autowired private val signUpUserRepository: UserRepository, + @Autowired private val reviewRepository: ReviewRepository ) : CustomDescribeSpec() { private val mockRestaurantID = "1" private val resource = "reviews" @@ -72,20 +73,142 @@ class ReviewIntegrationTest( .andExpect(jsonPath("$.result").value("SUCCESS")) .andReturn() - val actualResult: CommonResponse = objectMapper.readValue( - result.response.contentAsString.toByteArray(StandardCharsets.ISO_8859_1), - object : TypeReference>() {} - ) + val actualResult: CommonResponse = + objectMapper.readValue( + result.response.contentAsString.toByteArray(StandardCharsets.ISO_8859_1), + object : TypeReference>() {} + ) actualResult.data!!.review.content shouldBe "맛있어요" actualResult.data!!.review.isLike shouldBe false actualResult.data!!.review.imageUrls.size shouldBe 0 + + val getResult = mockMvc.perform( + MockMvcRequestBuilders.get( + "/v1/restaurants/reviews/{reviewId}", + actualResult.data!!.review.id + ) + ).andExpect(status().isOk()) + .andReturn() + + val reviewResult: CommonResponse = + objectMapper.readValue( + getResult.response.contentAsString.toByteArray(StandardCharsets.ISO_8859_1), + object : TypeReference>() {} + ) + reviewResult.data!!.review.content shouldBe "맛있어요" + } + + @WithMockUser(username = "test@gmail.com", roles = ["USER"], password = "a12345678") + @Transactional + @Test + fun `리뷰 수정 성공`() { + signUpUserService.signUpUser( + SignUpUserRequest( + email = "test@gmail.com", + password = "a12345678", + nickname = "testname" + ) + ) + val reviewRequest = ReviewRequestDto( + rating = 4.0, + content = "맛있어요", + imageUrls = listOf("image1", "image2", "image3") + ) + + val result = mockMvc.perform( + MockMvcRequestBuilders.post("/v1/restaurants/{restaurantID}/$resource", mockRestaurantID) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(reviewRequest)) + ) + .andReturn() + + val createResult: CommonResponse = + objectMapper.readValue( + result.response.contentAsString.toByteArray(StandardCharsets.ISO_8859_1), + object : TypeReference>() {} + ) + + val restaurantId = createResult.data?.review?.restaurantId + val reviewId = createResult.data?.review?.id + + val reviewUpdateRequest = UpdateReviewRequest( + ReviewRequestDto( + rating = 1.0, + content = "수정했어요", + imageUrls = listOf("update1", "update2") + ) + ) + + val updateResult = mockMvc.perform( + MockMvcRequestBuilders.patch("/v1/restaurants/reviews/{restaurantId}/reviews/{reviewId}", restaurantId, reviewId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(reviewUpdateRequest)) + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.result").value("SUCCESS")) + .andReturn() + + val actualResult: CommonResponse = + objectMapper.readValue( + updateResult.response.contentAsString.toByteArray(StandardCharsets.ISO_8859_1), + object : TypeReference>() {} + ) + + actualResult.data!!.review.content shouldBe reviewUpdateRequest.review.content + actualResult.data!!.review.imageUrls.size shouldBe reviewUpdateRequest.review.imageUrls.size } @WithMockUser(username = "test@gmail.com", roles = ["USER"], password = "a12345678") @Transactional @Test - fun`comment가 없으면 오류 반환`() { + fun `리뷰 삭제 성공`() { + signUpUserService.signUpUser( + SignUpUserRequest( + email = "test@gmail.com", + password = "a12345678", + nickname = "testname" + ) + ) + val reviewRequest = ReviewRequestDto( + rating = 4.0, + content = "맛있어요", + imageUrls = listOf("image1", "image2", "image3") + ) + + val result = mockMvc.perform( + MockMvcRequestBuilders.post("/v1/restaurants/{restaurantID}/$resource", mockRestaurantID) + .contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(reviewRequest)) + ).andReturn() + + val createResult: CommonResponse = + objectMapper.readValue( + result.response.contentAsString.toByteArray(StandardCharsets.ISO_8859_1), + object : TypeReference>() {} + ) + + val reviewId = createResult.data!!.review.id + + mockMvc.perform( + MockMvcRequestBuilders.delete( + "/v1/restaurants/reviews/{reviewId}", + reviewId + ) + ).andExpect(status().isOk).andExpect(jsonPath("$.result").value("SUCCESS")) + + mockMvc.perform( + MockMvcRequestBuilders.get( + "/v1/restaurants/reviews/{reviewId}", + reviewId + ) + ).andExpect(status().isBadRequest()) + .andExpect { result -> result.resolvedException is NotFoundReviewException } + } + + @WithMockUser(username = "test@gmail.com", roles = ["USER"], password = "a12345678") + @Transactional + @Test + fun `comment가 없으면 오류 반환`() { val reviewRequest = ReviewRequestDto( rating = 3.0, content = "", @@ -105,7 +228,6 @@ class ReviewIntegrationTest( @BeforeEach open fun setUp() { val user = User( - id = 1L, email = "test@gmail.com", nickname = "maker", password = "q1w2e3r4", @@ -136,10 +258,22 @@ class ReviewIntegrationTest( val reviewsSaved = reviewRepository.findAll() reviewsSaved.size shouldBe 20 + val reviewRequest = ReviewRequestDto( + rating = 4.0, + content = "맛있어요", + imageUrls = listOf() + ) + mockMvc.perform( + MockMvcRequestBuilders.post("/v1/restaurants/{restaurantID}/$resource", mockRestaurantID) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(reviewRequest)) + ) + val result = mockMvc.perform( MockMvcRequestBuilders.get("/v1/restaurants/reviews") .param("page", "0") .param("size", "5") + .param("sort", "createdAt,DESC") ) .andExpect(status().isOk) .andExpect(jsonPath("$.result").value("SUCCESS")) @@ -152,7 +286,274 @@ class ReviewIntegrationTest( val reviews = data["reviews"] as List> reviews.size shouldBe 5 + reviews.get(0)?.get("isLike") shouldBe false + reviews.get(0)?.get("viewCount") shouldBe 0 + + val result2 = mockMvc.perform( + MockMvcRequestBuilders.get("/v1/restaurants/reviews") + .param("page", "4") + .param("size", "5") + .param("sort", "createdAt,DESC") + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.result").value("SUCCESS")) + .andReturn() + val jsonMap2 = mapper.readValue>(result2.response.contentAsString) + val data2 = jsonMap2["data"] as Map + val reviews2 = data2["reviews"] as List> + + reviews2.size shouldBe 1 + } + + @Test + @WithMockUser(username = "test@gmail.com", roles = ["USER"]) + @Transactional + open fun `조회수 DESC 리스트 조회에 성공 (자신이 작성한 리뷰에 접근시 조회수가 반영되지 않는다`() { + val getReviews = reviewRepository.findAll() + val firstReviewId = getReviews.get(1).id + val secondReviewId = getReviews.get(2).id + + for (callCount in 1..3) { + mockMvc.perform( + MockMvcRequestBuilders.get( + "/v1/restaurants/reviews/{reviewId}", + firstReviewId + ) + ).andExpect(status().isOk()) + if (callCount != 3) { + mockMvc.perform( + MockMvcRequestBuilders.get( + "/v1/restaurants/reviews/{reviewId}", + secondReviewId + ) + ).andExpect(status().isOk()) + } + } + + val result = mockMvc.perform( + MockMvcRequestBuilders.get("/v1/restaurants/reviews") + .param("page", "0") + .param("size", "5") + .param("sort", "viewCount,DESC") + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.result").value("SUCCESS")) + .andReturn() + + val mapper = jacksonObjectMapper() + val jsonMap = mapper.readValue>(result.response.contentAsString) + + val data = jsonMap["data"] as Map + val reviews = data["reviews"] as List> + + reviews.size shouldBe 5 + reviews.get(0)?.get("viewCount") shouldBe 0 + } + + @Test + @WithMockUser(username = "another@gmail.com", roles = ["USER"]) + @Transactional + open fun `조회수 DESC 리스트 조회에 성공 (자신이 작성하지 않은 리뷰에 접근시 조회수가 반영된다`() { + signUpUserService.signUpUser( + SignUpUserRequest( + email = "another@gmail.com", + password = "a12345678", + nickname = "another" + ) + ) + val getReviews = reviewRepository.findAll() + val firstReviewId = getReviews.get(1).id + val secondReviewId = getReviews.get(2).id + + for (callCount in 1..3) { + mockMvc.perform( + MockMvcRequestBuilders.get( + "/v1/restaurants/reviews/{reviewId}", + firstReviewId + ) + ).andExpect(status().isOk()) + if (callCount != 3) { + mockMvc.perform( + MockMvcRequestBuilders.get( + "/v1/restaurants/reviews/{reviewId}", + secondReviewId + ) + ).andExpect(status().isOk()) + } + } + + val result = mockMvc.perform( + MockMvcRequestBuilders.get("/v1/restaurants/reviews") + .param("page", "0") + .param("size", "5") + .param("sort", "viewCount,DESC") + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.result").value("SUCCESS")) + .andReturn() + + val mapper = jacksonObjectMapper() + val jsonMap = mapper.readValue>(result.response.contentAsString) + + val data = jsonMap["data"] as Map + val reviews = data["reviews"] as List> + + reviews.size shouldBe 5 + reviews.get(0)?.get("viewCount") shouldBe 3 + reviews.get(1)?.get("viewCount") shouldBe 2 + reviews.get(2)?.get("viewCount") shouldBe 0 + } + + @Test + @WithMockUser(username = "another@gmail.com", roles = ["USER"]) + @Transactional + open fun `좋아요 DESC 리스트 조회에 성공`() { + signUpUserService.signUpUser( + SignUpUserRequest( + email = "another@gmail.com", + password = "a12345678", + nickname = "another" + ) + ) + val getReviews = reviewRepository.findAll() + val firstReviewId = getReviews.get(1).id + + val likeRequest = LikeReviewRequest(true) + + mockMvc.perform( + MockMvcRequestBuilders.post("/v1/restaurants/reviews/{reviewId}/like", firstReviewId) + .contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(likeRequest)) + ) + + val result = mockMvc.perform( + MockMvcRequestBuilders.get("/v1/restaurants/reviews") + .param("page", "0") + .param("size", "5") + .param("sort", "likeCount,DESC") + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.result").value("SUCCESS")) + .andReturn() + + val mapper = jacksonObjectMapper() + val jsonMap = mapper.readValue>(result.response.contentAsString) + + val data = jsonMap["data"] as Map + val reviews = data["reviews"] as List> + + reviews.size shouldBe 5 + reviews.get(0)?.get("likeCount") shouldBe 1 + reviews.get(1)?.get("likeCount") shouldBe 0 + } + + @Test + @WithMockUser(username = "newUser@gmail.com", roles = ["USER"]) + @Transactional + open fun `새로운 유저가 자기 자신의 리뷰 목록을 조회`() { + val newUser = User( + email = "newUser@gmail.com", + nickname = "maker1", + password = "q1w2e3r41", + withdrawal = false, + roles = listOf(), + profileImageUrl = "newuser-profile" + ) + + signUpUserRepository.save(newUser) + + val newReviews = (1..3).map { index -> + Review( + user = newUser, + restaurantId = index.toLong(), + content = "정말 맛있어요 $index", + rating = 5.0, + images = mutableListOf() + ) + } + + newReviews.forEach { reviewRepository.save(it) } + + mockMvc.perform( + MockMvcRequestBuilders.get( + "/v1/restaurants/reviews/{reviewId}", + newReviews.get(0).id + ) + ).andExpect(status().isOk()) + + val result = mockMvc.perform( + MockMvcRequestBuilders.get("/v1/restaurants/my-reviews") + .param("page", "0") + .param("size", "5") + .param("sort", "viewCount,DESC") + ).andExpect(status().isOk).andExpect(jsonPath("$.result").value("SUCCESS")).andReturn() + + val mapper = jacksonObjectMapper() + val jsonMap = mapper.readValue>(result.response.contentAsString) + + val data = jsonMap["data"] as Map + val reviews = data["reviews"] as List> + + reviews.size shouldBe 3 + + val reviewsSaved = reviewRepository.findAll() + reviewsSaved.size shouldBe 23 + } + + @Test + @WithMockUser(username = "newUser@gmail.com", roles = ["USER"]) + @Transactional + open fun `새로운 유저가 특정 리뷰 좋아요 확인`() { + val newUser = User( + email = "newUser@gmail.com", + nickname = "maker1", + password = "q1w2e3r41", + withdrawal = false, + roles = listOf(), + profileImageUrl = "newuser-profile" + ) + + signUpUserRepository.save(newUser) + + val likeRequest = LikeReviewRequest(true) + + val reviewId = reviewRepository.findAll().get(0).id + + val result = mockMvc.perform( + MockMvcRequestBuilders.post("/v1/restaurants/reviews/{reviewId}/like", reviewId) + .contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(likeRequest)) + ) + .andExpect(status().isOk).andExpect(jsonPath("$.result").value("SUCCESS")).andReturn() + + val actualResult: CommonResponse = + objectMapper.readValue( + result.response.contentAsString.toByteArray(StandardCharsets.ISO_8859_1), + object : TypeReference>() {} + ) + + actualResult.data!!.review.isLike shouldBe true + + mockMvc.perform( + MockMvcRequestBuilders.post("/v1/restaurants/reviews/{reviewId}/like", reviewId) + .contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(likeRequest)) + ) + .andExpect(status().isBadRequest) + .andExpect { result -> result.resolvedException is DuplicateLikeException } + + val unLikeRequest = LikeReviewRequest(false) + + val unlikeResult = mockMvc.perform( + MockMvcRequestBuilders.post("/v1/restaurants/reviews/{reviewId}/like", reviewId) + .contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(unLikeRequest)) + ) + .andExpect(status().isOk).andExpect(jsonPath("$.result").value("SUCCESS")).andReturn() + + val unLikeActualResult: CommonResponse = + objectMapper.readValue( + unlikeResult.response.contentAsString.toByteArray(StandardCharsets.ISO_8859_1), + object : TypeReference>() {} + ) + unLikeActualResult.data!!.review.isLike shouldBe false } } }