Skip to content

Commit

Permalink
feature: 봉달목록에 상품 추가 api (#31)
Browse files Browse the repository at this point in the history
* feat: CartProduct 도메인 추가

* feat: Entity 존재 여부 확인 확장 함수 추가

* feat: CartProduct 저장 로직 추가

* refactor: ProductController 패키지 이동

* feat: CartProduct 저장 API 추가

* test: CartProduct 저장시 예외 상황 테스트 추가

* refactor: 중복된 dependency 제거

* chore: mockk 의존성 추가

* test: 로그인 셋업 메서드 추가

* test: 구현된 Auth 헤더 사용 하도록 CartProduct API 테스트 수정

* refactor: 중복된 dependency 제거

* refactor: Accessor -> LoginMember 네이밍 수정

* test: 검증부 내용 이름 수정

* feat: 회원 존재 유무 검증 로직 추가

* test: application 테스트내 빈 등록 범위 수정

* test: 회원 검증 추가에 따른 테스트 로직 수정

* feat: 상품 수량 값객체 추가

* feat: 봉달 상품 수량 값객체 적용

* test: 잘못된 봉달 상품 수량에 대한 테스트 추가

* refactor: 이미 봉달 목록에 존재하는 상품에 대한 처리 추가

* fix: value 필드 예약어 사용에 의한 오류 해결
  • Loading branch information
TaeyeonRoyce authored Jan 31, 2024
1 parent 5e8e1e5 commit 2617f49
Show file tree
Hide file tree
Showing 26 changed files with 652 additions and 28 deletions.
26 changes: 14 additions & 12 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,34 +20,36 @@ repositories {
}

dependencies {
// kotlin
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")

// spring data jpa
implementation("org.springframework.boot:spring-boot-starter-data-jpa")

// spring boot web
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")

// jwt
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
implementation("io.jsonwebtoken:jjwt-impl:0.11.5")
implementation("io.jsonwebtoken:jjwt-jackson:0.11.5")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")

// kotlin jdsl
implementation("com.linecorp.kotlin-jdsl:jpql-dsl:3.3.0")
implementation("com.linecorp.kotlin-jdsl:jpql-render:3.3.0")

implementation("org.springframework.boot:spring-boot-starter-cache")

implementation("io.jsonwebtoken:jjwt-api:0.11.5")
// annotation processor
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")

// kotlin jdsl
implementation("com.linecorp.kotlin-jdsl:jpql-dsl:3.3.0")
implementation("com.linecorp.kotlin-jdsl:jpql-render:3.3.0")

// spring boot cache
implementation("org.springframework.boot:spring-boot-starter-cache")

runtimeOnly("com.mysql:mysql-connector-j")
runtimeOnly("com.h2database:h2")

annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
testImplementation("io.mockk:mockk:1.13.9")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.kotest:kotest-runner-junit5:5.4.2")
testImplementation("io.kotest:kotest-assertions-core:5.4.2")
Expand Down
40 changes: 40 additions & 0 deletions src/main/kotlin/com/petqua/application/cart/CartProductService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.petqua.application.cart

import com.petqua.application.cart.dto.SaveCartProductCommand
import com.petqua.common.domain.existByIdOrThrow
import com.petqua.domain.cart.CartProductRepository
import com.petqua.domain.member.MemberRepository
import com.petqua.domain.product.ProductRepository
import com.petqua.exception.cart.CartProductException
import com.petqua.exception.cart.CartProductExceptionType.DUPLICATED_PRODUCT
import com.petqua.exception.member.MemberException
import com.petqua.exception.member.MemberExceptionType.NOT_FOUND_MEMBER
import com.petqua.exception.product.ProductException
import com.petqua.exception.product.ProductExceptionType.NOT_FOUND_PRODUCT
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Transactional
@Service
class CartProductService(
private val cartProductRepository: CartProductRepository,
private val productRepository: ProductRepository,
private val memberRepository: MemberRepository,
) {

fun save(command: SaveCartProductCommand): Long {
memberRepository.existByIdOrThrow(command.memberId, MemberException(NOT_FOUND_MEMBER))
productRepository.existByIdOrThrow(command.productId, ProductException(NOT_FOUND_PRODUCT))
validateDuplicatedProduct(command)
return cartProductRepository.save(command.toCartProduct()).id
}

private fun validateDuplicatedProduct(command: SaveCartProductCommand) {
cartProductRepository.findByMemberIdAndProductIdAndIsMaleAndDeliveryMethod(
memberId = command.memberId,
productId = command.productId,
isMale = command.isMale,
deliveryMethod = command.deliveryMethod
)?.also { throw CartProductException(DUPLICATED_PRODUCT) }
}
}
23 changes: 23 additions & 0 deletions src/main/kotlin/com/petqua/application/cart/dto/CartProductDtos.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.petqua.application.cart.dto

import com.petqua.domain.cart.CartProduct
import com.petqua.domain.cart.CartProductQuantity
import com.petqua.domain.cart.DeliveryMethod

data class SaveCartProductCommand(
val memberId: Long,
val productId: Long,
val quantity: Int,
val isMale: Boolean,
val deliveryMethod: DeliveryMethod,
) {
fun toCartProduct(): CartProduct {
return CartProduct(
memberId = memberId,
productId = productId,
quantity = CartProductQuantity(quantity),
isMale = isMale,
deliveryMethod = deliveryMethod,
)
}
}
6 changes: 6 additions & 0 deletions src/main/kotlin/com/petqua/common/domain/Repository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,9 @@ import org.springframework.data.repository.findByIdOrNull
inline fun <reified T, ID> CrudRepository<T, ID>.findByIdOrThrow(
id: ID, e: Exception = IllegalArgumentException("${T::class.java.name} entity 를 찾을 수 없습니다. id=$id")
): T = findByIdOrNull(id) ?: throw e


inline fun <reified T, ID> CrudRepository<T, ID>.existByIdOrThrow(
id: ID,
e: Exception = IllegalArgumentException("${T::class.java.name} entity 를 찾을 수 없습니다. id=$id")
): Unit = if (!existsById(id!!)) throw e else Unit
5 changes: 5 additions & 0 deletions src/main/kotlin/com/petqua/common/util/Validate.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.petqua.common.util

inline fun throwExceptionWhen(condition: Boolean, exceptionSupplier: () -> RuntimeException) {
if (condition) throw exceptionSupplier()
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ package com.petqua.domain.auth

import com.petqua.domain.auth.token.AccessTokenClaims

class Accessor(
class LoginMember(
val memberId: Long,
val authority: Authority,
) {

companion object {
fun from(accessTokenClaims: AccessTokenClaims): Accessor {
return Accessor(
fun from(accessTokenClaims: AccessTokenClaims): LoginMember {
return LoginMember(
memberId = accessTokenClaims.memberId,
authority = accessTokenClaims.authority
)
Expand Down
35 changes: 35 additions & 0 deletions src/main/kotlin/com/petqua/domain/cart/CartProduct.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.petqua.domain.cart

import com.petqua.common.domain.BaseEntity
import jakarta.persistence.AttributeOverride
import jakarta.persistence.Column
import jakarta.persistence.Embedded
import jakarta.persistence.Entity
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id

@Entity
class CartProduct(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0L,

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

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

@Embedded
@AttributeOverride(name = "value", column = Column(name = "quantity", nullable = false))
val quantity: CartProductQuantity,

@Column(nullable = false)
val isMale: Boolean,

@Enumerated(value = EnumType.STRING)
@Column(nullable = false)
val deliveryMethod: DeliveryMethod,
) : BaseEntity()
21 changes: 21 additions & 0 deletions src/main/kotlin/com/petqua/domain/cart/CartProductQuantity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.petqua.domain.cart

import com.petqua.common.util.throwExceptionWhen
import com.petqua.exception.cart.CartProductException
import com.petqua.exception.cart.CartProductExceptionType.PRODUCT_QUANTITY_OVER_MAXIMUM
import com.petqua.exception.cart.CartProductExceptionType.PRODUCT_QUANTITY_UNDER_MINIMUM
import jakarta.persistence.Embeddable

private const val MIN_QUANTITY = 1
private const val MAX_QUANTITY = 99

@Embeddable
class CartProductQuantity(
val value: Int = 1,
) {

init {
throwExceptionWhen(value < MIN_QUANTITY) { CartProductException(PRODUCT_QUANTITY_UNDER_MINIMUM) }
throwExceptionWhen(value > MAX_QUANTITY) { CartProductException(PRODUCT_QUANTITY_OVER_MAXIMUM) }
}
}
13 changes: 13 additions & 0 deletions src/main/kotlin/com/petqua/domain/cart/CartProductRepository.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.petqua.domain.cart

import org.springframework.data.jpa.repository.JpaRepository

interface CartProductRepository : JpaRepository<CartProduct, Long> {

fun findByMemberIdAndProductIdAndIsMaleAndDeliveryMethod(
memberId: Long,
productId: Long,
isMale: Boolean,
deliveryMethod: DeliveryMethod
): CartProduct?
}
22 changes: 22 additions & 0 deletions src/main/kotlin/com/petqua/domain/cart/DeliveryMethod.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.petqua.domain.cart

import com.petqua.exception.cart.CartProductException
import com.petqua.exception.cart.CartProductExceptionType.INVALID_DELIVERY_METHOD
import java.util.Locale.ENGLISH

enum class DeliveryMethod(
val description: String,
) {

COMMON("일반 운송"),
SAFETY("안전 운송"),
PICK_UP("직접 방문"),
;

companion object {
fun from(name: String): DeliveryMethod {
return enumValues<DeliveryMethod>().find { it.name == name.uppercase(ENGLISH) }
?: throw CartProductException(INVALID_DELIVERY_METHOD)
}
}
}
13 changes: 13 additions & 0 deletions src/main/kotlin/com/petqua/exception/cart/CartProductException.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.petqua.exception.cart

import com.petqua.common.exception.BaseException
import com.petqua.common.exception.BaseExceptionType

class CartProductException(
private val exceptionType: CartProductExceptionType,
) : BaseException() {

override fun exceptionType(): BaseExceptionType {
return exceptionType
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.petqua.exception.cart

import com.petqua.common.exception.BaseExceptionType
import org.springframework.http.HttpStatus
import org.springframework.http.HttpStatus.BAD_REQUEST

enum class CartProductExceptionType(
private val httpStatus: HttpStatus,
private val errorMessage: String,
) : BaseExceptionType {

INVALID_DELIVERY_METHOD(httpStatus = BAD_REQUEST, errorMessage = "유효하지 않는 배송 방법입니다."),
PRODUCT_QUANTITY_UNDER_MINIMUM(httpStatus = BAD_REQUEST, errorMessage = "최소 1개 이상의 상품을 담을 수 있습니다."),
PRODUCT_QUANTITY_OVER_MAXIMUM(httpStatus = BAD_REQUEST, errorMessage = "최대 99개까지 구매할 수 있습니다."),
DUPLICATED_PRODUCT(httpStatus = BAD_REQUEST, errorMessage = "이미 장바구니에 담긴 상품입니다.")
;

override fun httpStatus(): HttpStatus {
return httpStatus
}

override fun errorMessage(): String {
return errorMessage
}
}
13 changes: 13 additions & 0 deletions src/main/kotlin/com/petqua/exception/member/MemberException.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.petqua.exception.member

import com.petqua.common.exception.BaseException
import com.petqua.common.exception.BaseExceptionType

class MemberException(
private val exceptionType: MemberExceptionType,
) : BaseException() {

override fun exceptionType(): BaseExceptionType {
return exceptionType
}
}
22 changes: 22 additions & 0 deletions src/main/kotlin/com/petqua/exception/member/MemberExceptionType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.petqua.exception.member

import com.petqua.common.exception.BaseExceptionType
import org.springframework.http.HttpStatus
import org.springframework.http.HttpStatus.NOT_FOUND

enum class MemberExceptionType(
private val httpStatus: HttpStatus,
private val errorMessage: String,
) : BaseExceptionType {

NOT_FOUND_MEMBER(NOT_FOUND, "존재하지 않는 회원입니다."),
;

override fun httpStatus(): HttpStatus {
return httpStatus
}

override fun errorMessage(): String {
return errorMessage
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package com.petqua.presentation.auth

import com.petqua.common.exception.auth.AuthException
import com.petqua.common.exception.auth.AuthExceptionType
import com.petqua.domain.auth.Accessor
import com.petqua.domain.auth.Auth
import com.petqua.domain.auth.LoginMember
import com.petqua.domain.auth.token.AuthTokenProvider
import com.petqua.domain.auth.token.RefreshTokenRepository
import jakarta.servlet.http.HttpServletRequest
Expand All @@ -25,29 +25,29 @@ class LoginArgumentResolver(

override fun supportsParameter(parameter: MethodParameter): Boolean {
return parameter.hasParameterAnnotation(Auth::class.java)
&& parameter.getParameterType() == Accessor::class.java
&& parameter.getParameterType() == LoginMember::class.java
}

override fun resolveArgument(
parameter: MethodParameter,
mavContainer: ModelAndViewContainer?,
webRequest: NativeWebRequest,
binderFactory: WebDataBinderFactory?
): Accessor {
): LoginMember {
val request = webRequest.getNativeRequest(HttpServletRequest::class.java)
?: throw AuthException(AuthExceptionType.INVALID_REQUEST)
val refreshToken = request.cookies?.find {it.name == REFRESH_TOKEN_COOKIE}?.value
val refreshToken = request.cookies?.find { it.name == REFRESH_TOKEN_COOKIE }?.value
val accessToken = webRequest.getHeader(HttpHeaders.AUTHORIZATION) as String
val accessTokenClaims = authTokenProvider.getAccessTokenClaims(accessToken)
if (refreshToken == null) {
return Accessor.from(accessTokenClaims)
return LoginMember.from(accessTokenClaims)
}

val savedRefreshToken = refreshTokenRepository.findByMemberId(accessTokenClaims.memberId)
?: throw AuthException(AuthExceptionType.INVALID_REFRESH_TOKEN)
if (savedRefreshToken.token == refreshToken) {
return Accessor.from(accessTokenClaims)
return LoginMember.from(accessTokenClaims)
}
throw AuthException(AuthExceptionType.INVALID_REFRESH_TOKEN)
}
}
}
Loading

0 comments on commit 2617f49

Please sign in to comment.