Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: 봉달목록에 상품 추가 api #31

Merged
merged 21 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6100145
feat: CartProduct 도메인 추가
TaeyeonRoyce Jan 29, 2024
042708b
feat: Entity 존재 여부 확인 확장 함수 추가
TaeyeonRoyce Jan 29, 2024
08c5168
feat: CartProduct 저장 로직 추가
TaeyeonRoyce Jan 29, 2024
28bf424
refactor: ProductController 패키지 이동
TaeyeonRoyce Jan 29, 2024
31f9f2d
feat: CartProduct 저장 API 추가
TaeyeonRoyce Jan 29, 2024
839b8dc
test: CartProduct 저장시 예외 상황 테스트 추가
TaeyeonRoyce Jan 29, 2024
e0bf5b3
refactor: 중복된 dependency 제거
TaeyeonRoyce Jan 29, 2024
9f677d7
chore: mockk 의존성 추가
TaeyeonRoyce Jan 29, 2024
d942e40
test: 로그인 셋업 메서드 추가
TaeyeonRoyce Jan 29, 2024
82b7dec
test: 구현된 Auth 헤더 사용 하도록 CartProduct API 테스트 수정
TaeyeonRoyce Jan 29, 2024
6fa783d
refactor: 중복된 dependency 제거
TaeyeonRoyce Jan 30, 2024
842404e
refactor: Accessor -> LoginMember 네이밍 수정
TaeyeonRoyce Jan 30, 2024
830ac68
test: 검증부 내용 이름 수정
TaeyeonRoyce Jan 30, 2024
c6e37a6
feat: 회원 존재 유무 검증 로직 추가
TaeyeonRoyce Jan 30, 2024
e67b0fc
test: application 테스트내 빈 등록 범위 수정
TaeyeonRoyce Jan 30, 2024
2348e94
test: 회원 검증 추가에 따른 테스트 로직 수정
TaeyeonRoyce Jan 30, 2024
d0a9c55
feat: 상품 수량 값객체 추가
TaeyeonRoyce Jan 30, 2024
18e179f
feat: 봉달 상품 수량 값객체 적용
TaeyeonRoyce Jan 30, 2024
b53c1f4
test: 잘못된 봉달 상품 수량에 대한 테스트 추가
TaeyeonRoyce Jan 30, 2024
096a109
refactor: 이미 봉달 목록에 존재하는 상품에 대한 처리 추가
TaeyeonRoyce Jan 31, 2024
5712afa
fix: value 필드 예약어 사용에 의한 오류 해결
TaeyeonRoyce Jan 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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.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))
val savedCartProduct = cartProductRepository.save(command.toCartProduct())
return savedCartProduct.id
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 근데 장바구니에 같은 상품이 두 개 저장될 수 있나요? 보통 이미 동일한 상품이 장바구니에 존재하면 수량만 늘어났던 것 같아서요! 이 부분 펫쿠아 룰을 모르겠네요...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

엇 그렇네요. 이미 담긴 상품에 대해서 추가하는 경우에 대한 정책을 생각 못했네요..
이 부분 스크럼때 여쭤보고 반영 할게요!
감사합니다👍

}
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
33 changes: 33 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,33 @@
package com.petqua.domain.cart

import com.petqua.common.domain.BaseEntity
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
val quantity: CartProductQuantity,

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

@Enumerated(value = EnumType.STRING)
@Column(nullable = false)
val deliveryMethod: DeliveryMethod,
) : BaseEntity()
23 changes: 23 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,23 @@
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.Column
import jakarta.persistence.Embeddable

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

@Embeddable
class CartProductQuantity(
@Column(nullable = false)
val quantity: Int = 1,
) {

init {
throwExceptionWhen(quantity < MIN_QUANTITY) { CartProductException(PRODUCT_QUANTITY_UNDER_MINIMUM) }
throwExceptionWhen(quantity > MAX_QUANTITY) { CartProductException(PRODUCT_QUANTITY_OVER_MAXIMUM) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.petqua.domain.cart

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

interface CartProductRepository : JpaRepository<CartProduct, Long>
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)
}
}
}
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,24 @@
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개까지 구매할 수 있습니다."),
;

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
}
}
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)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.petqua.presentation.cart

import com.petqua.application.cart.CartProductService
import com.petqua.application.product.dto.ProductDetailResponse
import com.petqua.domain.auth.Auth
import com.petqua.domain.auth.LoginMember
import com.petqua.presentation.cart.dto.SaveCartProductRequest
import org.springframework.http.ResponseEntity
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 org.springframework.web.servlet.support.ServletUriComponentsBuilder

@RequestMapping("/carts")
@RestController
class CartProductController(
private val cartProductService: CartProductService,
) {

@PostMapping
fun save(
@Auth loginMember: LoginMember,
@RequestBody request: SaveCartProductRequest
): ResponseEntity<ProductDetailResponse> {
val command = request.toCommand(loginMember.memberId)
val cartProductId = cartProductService.save(command)
val location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/items/{id}")
.buildAndExpand(cartProductId)
.toUri()
return ResponseEntity.created(location).build()
Comment on lines +28 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 친구는 무엇인가요?

ResponseEntity.created(URI.create("/carts" + cartProductId)).build();

위 코드와 같은 걸까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Location 헤더에 값이 달라져요!
위 방식으로 하면
"http://localhost:53077/carts/items/1" 이렇게 현재 온 요청의 url을 기반으로 추가되고,

URI.create("/carts" + cartProductId)).build();
이렇게 하면 "/carts/items/1" 이런 응답이 옵니다.

생성된 자원에 대해 접근가능한 URL을 제공하려고 사용했어요!!

}
}
Loading
Loading