Skip to content

Commit

Permalink
[KAN-39] Review 작성 API & 테스트코드 초안 작성 (#33)
Browse files Browse the repository at this point in the history
* Restaurant Review Entity 추가

* feat: Review Creat API
fix: final BaseEntity to non-final

* test: 리뷰 작성 간단한  테스트 코드 추가

* chore: lint apply

* chore: lint apply again

* test: MethodArguementNotValidException Handling

* chore: lint apply

* refactor: 1. restaurantId nullable 수정
2. nullable=false 생략
3. 별점 Integer To Double 수정
4. 리뷰 작성 Response에 Entity가 아닌 DTO로 수정+swagger

* chore: lint apply

* fix: Entity Column nullable option

* chore: Review, ReviewDto 자잘한 수정

* feat: ReviewLike Entity추가 및 로직 반영

* chore: lint apply

* feat: review list 조회 기능

* chore: lint applies

* fix: queryDSL 반영

* Revert "chore: lint applies"

This reverts commit 6264f0b.

* fix: queryDSL 쿼리 반영

* refactor: remove PrincipalUtils

* refactor: naming again ReviewDto field

* refactor: 자잘한 수정들

* refactor: queryDSL 계층 구조 분리

* [KAN-39] 리뷰 생성 API - 일부 수정 (#43)

* [KAN-39] 리뷰 생성 API - 일부 수정

* [KAN-39] 리뷰 생성 API - test code

* [KAN-39] 리뷰 생성 API - test code

---------

Co-authored-by: Bob Sin <tkagmd1@naver.com>
  • Loading branch information
goathoon and sinkyoungdeok authored May 16, 2024
1 parent b360342 commit 770cec2
Show file tree
Hide file tree
Showing 18 changed files with 498 additions and 28 deletions.
3 changes: 2 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ dependencies {
testImplementation("junit", "junit", "4.13.2")
testImplementation("io.kotest:kotest-runner-junit5:5.5.4")
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.2")
testImplementation("io.mockk:mockk:1.12.4")
testImplementation("io.mockk:mockk:1.12.0")
testImplementation("org.springframework.security:spring-security-test")

// TestContainers
testImplementation("org.testcontainers:testcontainers:1.17.1")
Expand Down
10 changes: 5 additions & 5 deletions src/main/kotlin/com/restaurant/be/common/entity/BaseEntity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import javax.persistence.MappedSuperclass

@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
open class BaseEntity {
class BaseEntity {

@CreatedDate
final var createdAt: LocalDateTime = LocalDateTime.now()
private set
var createdAt: LocalDateTime = LocalDateTime.now()
set

@LastModifiedDate
final var modifiedAt: LocalDateTime = LocalDateTime.now()
private set
var modifiedAt: LocalDateTime = LocalDateTime.now()
set
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.restaurant.be.common.response.ErrorCode
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.validation.FieldError
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestControllerAdvice
Expand Down Expand Up @@ -46,7 +47,7 @@ class GlobalExceptionHandler {
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = [WebExchangeBindException::class])
@ExceptionHandler(value = [WebExchangeBindException::class, MethodArgumentNotValidException::class])
fun methodArgumentNotValidException(
e: WebExchangeBindException
): CommonResponse<String> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,7 @@ data class NotEqualTokenException(
data class NotFoundUserException(
override val message: String = "존재 하지 않는 유저 입니다."
) : ServerException(400, message)

data class NotFoundReviewException(
override val message: String = "존재하지 않은 리뷰 입니다."
) : ServerException(400, message)
62 changes: 62 additions & 0 deletions src/main/kotlin/com/restaurant/be/review/domain/entity/Review.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.restaurant.be.review.domain.entity

import com.restaurant.be.common.entity.BaseEntity
import com.restaurant.be.review.presentation.dto.common.ReviewResponseDto
import com.restaurant.be.user.domain.entity.User
import kotlinx.serialization.json.JsonNull.content
import javax.persistence.CascadeType
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.FetchType
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id
import javax.persistence.JoinColumn
import javax.persistence.ManyToOne
import javax.persistence.OneToMany
import javax.persistence.Table

@Entity
@Table(name = "restaurant_reviews")
class Review(
@Id
@Column(name = "review_id")
@GeneratedValue(strategy = GenerationType.AUTO)
val id: Long? = null,

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
val user: User,

@Column(nullable = false)
val restaurantId: Long,

@Column(nullable = false)
val content: String,

@Column(nullable = false)
val rating: Double,

// 부모 (Review Entity)가 주인이되어 Image참조 가능. 반대는 불가능
@OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true)
@JoinColumn(name = "review_id")
val images: MutableList<ReviewImage> = mutableListOf()

) : BaseEntity() {
fun addImage(reviewImage: ReviewImage) {
images.add(reviewImage)
}

fun toResponseDTO(doesUserLike: Boolean): ReviewResponseDto {
return ReviewResponseDto(
userId = user.id ?: 0,
username = user.nickname,
profileImageUrl = user.profileImageUrl,
restaurantId = restaurantId,
rating = rating,
content = content,
imageUrls = images.map { it.imageUrl },
isLike = doesUserLike
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.restaurant.be.review.domain.entity

import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id
import javax.persistence.Table

@Entity
@Table(name = "review_images")
class ReviewImage(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,

@Column(nullable = false, length = 300)
val imageUrl: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.restaurant.be.review.domain.entity

import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id
import javax.persistence.Table

@Entity
@Table(name = "review_likes")
class ReviewLikes(
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
val id: Long? = null,

@Column(name = "user_id", nullable = false)
val userId: Long,

@Column(name = "review_id", nullable = false)
val reviewId: Long
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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.domain.entity.ReviewImage
import com.restaurant.be.review.presentation.dto.CreateReviewResponse
import com.restaurant.be.review.presentation.dto.common.ReviewRequestDto
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

@Service
class CreateReviewService(
private val reviewRepository: ReviewRepository,
private val userRepository: UserRepository
) {
@Transactional
fun createReview(
restaurantId: Long,
reviewRequest: ReviewRequestDto,
email: String
): CreateReviewResponse {
val user = userRepository.findByEmail(email)
?: throw NotFoundUserEmailException()

val review = reviewRequest.toEntity(user, restaurantId)

reviewRequest.imageUrls.forEach {
review.addImage(
ReviewImage(
imageUrl = it
)
)
}

reviewRepository.save(review)

val reviewWithLikes = reviewRepository.findReview(user, review.id ?: 0)
?: throw NotFoundReviewException()

val responseDto = ReviewResponseDto.toDto(
reviewWithLikes.review,
reviewWithLikes.isLikedByUser
)

return CreateReviewResponse(responseDto)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.restaurant.be.review.domain.service

import com.restaurant.be.common.exception.NotFoundUserEmailException
import com.restaurant.be.review.presentation.dto.GetReviewResponse
import com.restaurant.be.review.repository.ReviewLikesRepository
import com.restaurant.be.review.repository.ReviewRepository
import com.restaurant.be.user.repository.UserRepository
import org.springframework.data.domain.PageRequest
import org.springframework.stereotype.Service

@Service
class GetReviewService(
private val userRepository: UserRepository,
private val reviewRepository: ReviewRepository,
private val reviewLikesRepository: ReviewLikesRepository
) {
fun getReviewListOf(page: Int, size: Int, email: String): GetReviewResponse {
val pageable = PageRequest.of(page, size)
val reviews = reviewRepository.findAll(pageable).content

val user = userRepository.findByEmail(email)
?: throw NotFoundUserEmailException()

return GetReviewResponse(
reviews.map {
it
.toResponseDTO(doesUserLike = isReviewLikedByUser(user.id, it.id))
}
)
}

fun isReviewLikedByUser(userId: Long?, reviewId: Long?): Boolean {
if (userId != 0L) {
return reviewLikesRepository.existsByReviewIdAndUserId(userId, reviewId)
}
return false
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.restaurant.be.review.presentation.controller

import com.restaurant.be.common.response.CommonResponse
import com.restaurant.be.review.domain.service.CreateReviewService
import com.restaurant.be.review.presentation.dto.CreateReviewResponse
import com.restaurant.be.review.presentation.dto.common.ReviewRequestDto
import io.swagger.annotations.Api
import io.swagger.annotations.ApiOperation
import io.swagger.v3.oas.annotations.media.Content
Expand All @@ -10,14 +12,18 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse
import org.springframework.security.access.prepost.PreAuthorize
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.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.security.Principal
import javax.validation.Valid

@Api(tags = ["03. Review Info"], description = "리뷰 서비스")
@RestController
@RequestMapping("/v1/restaurants")
class CreateReviewController {
class CreateReviewController(
private val createReviewService: CreateReviewService
) {

@PostMapping("/{restaurantId}/reviews")
@PreAuthorize("hasRole('USER')")
Expand All @@ -29,8 +35,11 @@ class CreateReviewController {
)
fun createReview(
principal: Principal,
@PathVariable restaurantId: String
@PathVariable restaurantId: Long,
@Valid @RequestBody
request: ReviewRequestDto
): CommonResponse<CreateReviewResponse> {
return CommonResponse.success()
val response = createReviewService.createReview(restaurantId, request, principal.name)
return CommonResponse.success(response)
}
}
Original file line number Diff line number Diff line change
@@ -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.GetReviewService
import com.restaurant.be.review.presentation.dto.GetReviewResponse
import io.swagger.annotations.Api
import io.swagger.annotations.ApiOperation
Expand All @@ -10,13 +11,16 @@ 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.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.security.Principal

@Api(tags = ["03. Review Info"], description = "리뷰 서비스")
@RestController
@RequestMapping("/v1/restaurants/reviews")
class GetReviewController {
class GetReviewController(
private val getReviewService: GetReviewService
) {

@GetMapping
@PreAuthorize("hasRole('USER')")
Expand All @@ -27,8 +31,11 @@ class GetReviewController {
content = [Content(schema = Schema(implementation = GetReviewResponse::class))]
)
fun getReview(
principal: Principal
principal: Principal,
@RequestParam page: Int,
size: Int
): CommonResponse<GetReviewResponse> {
return CommonResponse.success()
val response = getReviewService.getReviewListOf(page, size, principal.name)
return CommonResponse.success(response)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.restaurant.be.review.presentation.dto

import com.restaurant.be.review.domain.entity.Review

data class ReviewWithLikesDto(
val review: Review,
val isLikedByUser: Boolean
)
Loading

0 comments on commit 770cec2

Please sign in to comment.