Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions docs/order_diagram.mermaid
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
erDiagram
Order {
%% 고객의 주문 단위
}
OrderLine {
%% 주문에 포함된 개별 주문 항목
}
Order ||--o{ OrderLine: "주문 - 주문 항목 (1:N)"
25 changes: 25 additions & 0 deletions src/main/kotlin/com/nilgil/commerce/common/StringListConverter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.nilgil.commerce.common

import jakarta.persistence.AttributeConverter
import jakarta.persistence.Converter

@Converter
class StringListConverter : AttributeConverter<List<String>, String> {
companion object {
const val DELIMITER = ","
}

override fun convertToDatabaseColumn(attribute: List<String>?): String? {
if (attribute.isNullOrEmpty()) {
return null
}
return attribute.joinToString(DELIMITER)
}

override fun convertToEntityAttribute(dbData: String?): List<String> {
if (dbData.isNullOrBlank()) {
return emptyList()
}
return dbData.split(DELIMITER).map { it.trim() }
}
}
51 changes: 51 additions & 0 deletions src/main/kotlin/com/nilgil/commerce/order/Order.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.nilgil.commerce.order

import com.nilgil.commerce.common.BaseEntity
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.Table

@Entity
@Table(name = "orders")
Copy link
Collaborator

@f-lab-seb f-lab-seb Jul 13, 2025

Choose a reason for hiding this comment

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

product, seller ...
table name 이 단수형이었었는데 orders 만 별도로 복수형으로 하신 이유가 궁금해욥

class Order(
@Column(unique = true)
val code: String,
val totalAmount: Int,
val userId: Long,
) : BaseEntity() {
@Enumerated(EnumType.STRING)
var status: OrderStatus = OrderStatus.CREATED

var paymentId: Long? = null

fun pay(paymentId: Long) {
if (this.status != OrderStatus.CREATED) {
throw IllegalStateException("결제를 진행할 수 없는 상태입니다.")
}
this.paymentId = paymentId
this.status = OrderStatus.PAID
}

fun complete() {
if (this.status != OrderStatus.PAID) {
throw IllegalStateException("완료 처리할 수 없는 상태입니다.")
}
this.status = OrderStatus.COMPLETED
}

fun cancel() {
if (this.status != OrderStatus.CREATED && this.status != OrderStatus.PAID) {
throw IllegalStateException("취소할 수 없는 상태입니다.")
}
this.status = OrderStatus.CANCELLED
}

fun returnOrder() {
if (this.status != OrderStatus.COMPLETED) {
throw IllegalStateException("반품할 수 없는 상태입니다.")
}
this.status = OrderStatus.RETURNED
}
}
25 changes: 25 additions & 0 deletions src/main/kotlin/com/nilgil/commerce/order/OrderItem.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.nilgil.commerce.order

import com.nilgil.commerce.common.StringListConverter
import jakarta.persistence.Column
import jakarta.persistence.Convert
import jakarta.persistence.Embeddable

@Embeddable
data class OrderItem(
@Column(name = "item_title")
val title: String,
@Convert(converter = StringListConverter::class)
@Column(name = "item_options")
val options: List<String> = listOf(),
Comment on lines +12 to +14
Copy link
Collaborator

Choose a reason for hiding this comment

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

options 에 어떤게 들어가는 거에용?
요로케 String 으로 저장했을 때에 어떤 장단점이 있을지 설명 부탁드리겠습니다~

@Column(name = "item_price")
val price: Int,
@Column(name = "item_thumbnail_image_url")
val thumbnailImageUrl: String? = null,
@Column(name = "product_item_id")
val productItemId: Long,
) {
init {
require(price >= 0) { "가격은 0 이상이어야 합니다." }
}
}
22 changes: 22 additions & 0 deletions src/main/kotlin/com/nilgil/commerce/order/OrderLine.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.nilgil.commerce.order

import com.nilgil.commerce.common.BaseEntity
import jakarta.persistence.Embedded
import jakarta.persistence.Entity
import jakarta.persistence.FetchType
import jakarta.persistence.ManyToOne

@Entity
class OrderLine(
@Embedded
val item: OrderItem,
val quantity: Int,
@ManyToOne(fetch = FetchType.LAZY)
val order: Order,
) : BaseEntity() {
init {
require(quantity >= 1) { "수량은 1개 이상이어야 합니다." }
}

fun getTotalPrice(): Int = item.price * quantity
}
9 changes: 9 additions & 0 deletions src/main/kotlin/com/nilgil/commerce/order/OrderStatus.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.nilgil.commerce.order

enum class OrderStatus {
CREATED,
PAID,
COMPLETED,
CANCELLED,
RETURNED,
}
41 changes: 41 additions & 0 deletions src/test/kotlin/com/nilgil/commerce/order/OrderFixtures.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.nilgil.commerce.order

import org.mockito.Mockito.mock

object OrderFixtures {
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍👍👍👍
이미 아실 수도 있지만, 조금 더 객체 생성이 복잡해지면 kotest 의 Fixture 를 사용해도 좋을 것 같아요

fun anOrder(
code: String = "TEST-ORDER-CODE-12345",
totalAmount: Int = 0,
userId: Long = 1L,
): Order =
Order(
code = code,
totalAmount = totalAmount,
userId = userId,
)

fun anOrderLine(
order: Order = mock(Order::class.java),
quantity: Int = 1,
item: OrderItem = anOrderItem(),
): OrderLine =
OrderLine(
order = order,
quantity = quantity,
item = item,
)

fun anOrderItem(
title: String = "테스트 상품",
options: List<String> = listOf("옵션1", "옵션2"),
price: Int = 10000,
productItemId: Long = 1L,
): OrderItem =
OrderItem(
title = title,
options = options,
price = price,
productItemId = productItemId,
thumbnailImageUrl = "http://test.image/1",
)
}
33 changes: 33 additions & 0 deletions src/test/kotlin/com/nilgil/commerce/order/OrderItemTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.nilgil.commerce.order

import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test

class OrderItemTest {
@Nested
@DisplayName("객체 생성 시")
inner class DescribeCreation {
@Test
fun `가격이 음수이면 IllegalArgumentException이 발생한다`() {
// when, then
assertThatIllegalArgumentException()
.isThrownBy {
OrderFixtures.anOrderItem(price = -10000)
}
}

@Test
fun `가격이 0 또는 양수이면 정상적으로 생성된다`() {
// when
val itemWithZeroPrice = OrderFixtures.anOrderItem(price = 0)
val itemWithPositivePrice = OrderFixtures.anOrderItem(price = 10000)

// then
assertThat(itemWithZeroPrice).isNotNull
assertThat(itemWithPositivePrice).isNotNull
}
}
}
56 changes: 56 additions & 0 deletions src/test/kotlin/com/nilgil/commerce/order/OrderLineTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.nilgil.commerce.order

import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource

class OrderLineTest {
@Nested
@DisplayName("객체 생성 시")
inner class DescribeCreation {
@CsvSource("0", "-1", Integer.MIN_VALUE.toString())
@ParameterizedTest
fun `수량이 0 또는 음수이면 IllegalArgumentException이 발생한다`(quantity: Int) {
// when, then
assertThatIllegalArgumentException()
.isThrownBy {
OrderFixtures.anOrderLine(quantity = quantity)
}
}

@CsvSource("1", Integer.MAX_VALUE.toString())
@ParameterizedTest
fun `수량이 양수이면 정상적으로 생성된다`(quantity: Int) {
// when
val orderLine = OrderFixtures.anOrderLine(quantity = quantity)

// then
assertThat(orderLine).isNotNull
assertThat(orderLine.quantity).isEqualTo(quantity)
}
}

@Nested
@DisplayName("총액 조회 시")
inner class DescribeGetTotalPrice {
@Test
fun `아이템 가격과 수량을 곱한 총액을 반환한다`() {
// given
val price = 15000
val item = OrderFixtures.anOrderItem(price = price)

val quantity = 3
val orderLine = OrderFixtures.anOrderLine(item = item, quantity = quantity)

// when
val totalPrice = orderLine.getTotalPrice()

// then
assertThat(totalPrice).isEqualTo(price * quantity)
}
}
}
Loading
Loading