From bbc1e8cbf29f56150dd5d629326d4aaf3e7b1a2c Mon Sep 17 00:00:00 2001 From: songhansol Date: Tue, 24 Feb 2026 18:27:37 +0900 Subject: [PATCH 01/22] =?UTF-8?q?=EC=9E=91=EC=84=B1=EC=A4=91..?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/dto/TicketCreationDto.kt | 2 +- .../application/service/TicketService.kt | 27 ---------- .../service/TicketService/TicketService.kt | 15 ++++++ .../{enumeration => enum}/TicketStatus.kt | 2 +- .../example/ticket/domain/enum/TicketType.kt | 6 +++ .../ticket/domain/enumeration/TicketType.kt | 6 --- .../org/example/ticket/domain/model/Ticket.kt | 53 +++++++++++-------- .../ticket/infra/api/MelonTicketApiClient.kt | 20 ------- .../ticket/infra/api/NolTicketClient.kt | 18 ------- .../ticket/infra/api/TicketApiClient.kt | 9 ---- .../infra/repository/TicketJpaRepository.kt | 7 --- .../api/MelonTicketApiClient.kt | 17 ++++++ .../infrastructure/api/NolTicketApiClient.kt | 16 ++++++ .../infrastructure/api/TicketApiClient.kt | 8 +++ .../dto/TicketApiResponseDto.kt | 10 ++-- .../ticket/presentation/TicketController.kt | 19 ------- 16 files changed, 100 insertions(+), 135 deletions(-) delete mode 100644 src/main/kotlin/org/example/ticket/application/service/TicketService.kt create mode 100644 src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt rename src/main/kotlin/org/example/ticket/domain/{enumeration => enum}/TicketStatus.kt (62%) create mode 100644 src/main/kotlin/org/example/ticket/domain/enum/TicketType.kt delete mode 100644 src/main/kotlin/org/example/ticket/domain/enumeration/TicketType.kt delete mode 100644 src/main/kotlin/org/example/ticket/infra/api/MelonTicketApiClient.kt delete mode 100644 src/main/kotlin/org/example/ticket/infra/api/NolTicketClient.kt delete mode 100644 src/main/kotlin/org/example/ticket/infra/api/TicketApiClient.kt delete mode 100644 src/main/kotlin/org/example/ticket/infra/repository/TicketJpaRepository.kt create mode 100644 src/main/kotlin/org/example/ticket/infrastructure/api/MelonTicketApiClient.kt create mode 100644 src/main/kotlin/org/example/ticket/infrastructure/api/NolTicketApiClient.kt create mode 100644 src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClient.kt rename src/main/kotlin/org/example/ticket/{infra => infrastructure}/dto/TicketApiResponseDto.kt (55%) delete mode 100644 src/main/kotlin/org/example/ticket/presentation/TicketController.kt diff --git a/src/main/kotlin/org/example/ticket/application/dto/TicketCreationDto.kt b/src/main/kotlin/org/example/ticket/application/dto/TicketCreationDto.kt index 29339a3..1f66070 100644 --- a/src/main/kotlin/org/example/ticket/application/dto/TicketCreationDto.kt +++ b/src/main/kotlin/org/example/ticket/application/dto/TicketCreationDto.kt @@ -1,5 +1,5 @@ package org.example.ticket.application.dto data class TicketCreationDto( - val varCode: String + val barcode: String ) diff --git a/src/main/kotlin/org/example/ticket/application/service/TicketService.kt b/src/main/kotlin/org/example/ticket/application/service/TicketService.kt deleted file mode 100644 index 1e731a3..0000000 --- a/src/main/kotlin/org/example/ticket/application/service/TicketService.kt +++ /dev/null @@ -1,27 +0,0 @@ -package org.example.ticket.application.service - -import org.example.ticket.application.dto.TicketCreationDto -import org.example.ticket.domain.model.Ticket -import org.example.ticket.infra.api.TicketApiClient -import org.example.ticket.infra.repository.TicketJpaRepository -import org.springframework.stereotype.Service - -@Service -class TicketService( - private val ticketApiClient: List, - private val ticketRepository: TicketJpaRepository -) { - fun createTicket(ticketCreationDto: TicketCreationDto) { - val ticketType = Ticket.ticketType(ticketCreationDto.varCode) - val apiClient = ticketApiClient.first { it.type(ticketCreationDto.varCode) == ticketType } - - val ticketResponseDto = apiClient.getTicket(ticketCreationDto.varCode) - val ticket = ticketResponseDto.toTicket() - - ticketRepository.save(ticket) - } - - fun applySellerOfferPrice() { - // TODO : 구현 - } -} diff --git a/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt b/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt new file mode 100644 index 0000000..be53671 --- /dev/null +++ b/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt @@ -0,0 +1,15 @@ +package org.example.ticket.application.service.TicketService + +import org.example.ticket.application.dto.TicketCreationDto +import org.example.ticket.domain.model.Ticket +import org.springframework.stereotype.Service + +@Service +class TicketService ( + +) { + fun createTicket(ticketCreatioNDto: TicketCreationDto){ + val ticketType = Ticket. + + }; +} \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/domain/enumeration/TicketStatus.kt b/src/main/kotlin/org/example/ticket/domain/enum/TicketStatus.kt similarity index 62% rename from src/main/kotlin/org/example/ticket/domain/enumeration/TicketStatus.kt rename to src/main/kotlin/org/example/ticket/domain/enum/TicketStatus.kt index 2e6260c..715cb8f 100644 --- a/src/main/kotlin/org/example/ticket/domain/enumeration/TicketStatus.kt +++ b/src/main/kotlin/org/example/ticket/domain/enum/TicketStatus.kt @@ -1,4 +1,4 @@ -package org.example.ticket.domain.enumeration +package org.example.ticket.domain.enum enum class TicketStatus { ON_SALE, diff --git a/src/main/kotlin/org/example/ticket/domain/enum/TicketType.kt b/src/main/kotlin/org/example/ticket/domain/enum/TicketType.kt new file mode 100644 index 0000000..28d8e4b --- /dev/null +++ b/src/main/kotlin/org/example/ticket/domain/enum/TicketType.kt @@ -0,0 +1,6 @@ +package org.example.ticket.domain.enum + +enum class TicketType { + MELON, + NOL +} \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/domain/enumeration/TicketType.kt b/src/main/kotlin/org/example/ticket/domain/enumeration/TicketType.kt deleted file mode 100644 index ad89680..0000000 --- a/src/main/kotlin/org/example/ticket/domain/enumeration/TicketType.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.example.ticket.domain.enumeration - -enum class TicketType { - NOL, - MELON -} diff --git a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt index de898a3..2ac15bb 100644 --- a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt +++ b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt @@ -1,43 +1,50 @@ package org.example.ticket.domain.model -import org.example.ticket.domain.enumeration.TicketStatus -import org.example.ticket.domain.enumeration.TicketType +import org.example.ticket.domain.enum.TicketStatus +import org.example.ticket.domain.enum.TicketType +import org.springframework.cglib.core.Local import java.math.BigDecimal import java.time.LocalDateTime class Ticket( - val id: Long? = null, - val expirationAt: LocalDateTime, - val originalPrice: BigDecimal, - private var sellingPrice: BigDecimal? = null, - val ticketStatus: TicketStatus = TicketStatus.ON_SALE, + val id: Long? = null, // ? => DB에 넘기기전에 id를 안넣으려고 (자동생성) + val barcode: String, + val eventDateTime: LocalDateTime, + val originalPrice: BigDecimal, // BigDecimal => 10진수로 저장(소수점 계산을 위해) + val sellingPrice: BigDecimal? = null, + var ticketStatus: TicketStatus = TicketStatus.ON_SALE, + var ticketType: TicketType? = null ) { companion object { - val ACTIVATION_DEAD_LINE: LocalDateTime = LocalDateTime.now().plusHours(3L) - private val HALF = BigDecimal("0.5") + fun ticketBarcodeCheck(barcode: String) { + require(barcode.length == 8) { "바코드는 8자리여야 합니다." } + } + + fun ticketTimeCheck(eventTime: LocalDateTime) { + val deadLine: LocalDateTime = eventTime.plusHours(3) + require(eventTime.isAfter(deadLine)) { + "공연 시작 3시간 전까지만 등록할 수 있습니다." + } + } - fun ticketType(varCode: String): TicketType { - return if (varCode.contains("MELON")) { + fun ticketTypeCheck(barcode: String): TicketType { + return if (barcode.all { it.isLetter() }) { TicketType.MELON } else { TicketType.NOL } } + + } init { - require(expirationAt.isAfter(ACTIVATION_DEAD_LINE)) { "ticket cannot be registered after expiration" } + ticketBarcodeCheck(barcode); + ticketTimeCheck(eventDateTime); + ticketType = ticketTypeCheck(barcode); } - fun applySellerOfferPrice(offerPrice: BigDecimal) { - val isPerformanceDay = LocalDateTime.now().toLocalDate() == expirationAt.toLocalDate() - if (isPerformanceDay) { - val maxAllowedPrice = originalPrice.multiply(HALF) - require(offerPrice <= maxAllowedPrice) { - "on performance day, selling price must be <= 50% of regular price" - } - } - - this.sellingPrice = offerPrice - } } + + + diff --git a/src/main/kotlin/org/example/ticket/infra/api/MelonTicketApiClient.kt b/src/main/kotlin/org/example/ticket/infra/api/MelonTicketApiClient.kt deleted file mode 100644 index 6737a08..0000000 --- a/src/main/kotlin/org/example/ticket/infra/api/MelonTicketApiClient.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.example.ticket.infra.api - -import org.example.ticket.domain.enumeration.TicketType -import org.example.ticket.infra.dto.TicketApiResponseDto -import org.springframework.stereotype.Component -import java.math.BigDecimal -import java.time.LocalDateTime - -@Component -class MelonTicketApiClient : TicketApiClient { - override fun getTicket(varCode: String): TicketApiResponseDto { - return TicketApiResponseDto( - LocalDateTime.now().plusDays(20), BigDecimal.valueOf(10000) - ) - } - - override fun type(varCode: String): TicketType { - return TicketType.MELON - } -} diff --git a/src/main/kotlin/org/example/ticket/infra/api/NolTicketClient.kt b/src/main/kotlin/org/example/ticket/infra/api/NolTicketClient.kt deleted file mode 100644 index 0b289cc..0000000 --- a/src/main/kotlin/org/example/ticket/infra/api/NolTicketClient.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.example.ticket.infra.api - -import org.example.ticket.domain.enumeration.TicketType -import org.example.ticket.infra.dto.TicketApiResponseDto -import org.springframework.stereotype.Component -import java.math.BigDecimal -import java.time.LocalDateTime - -@Component -class NolTicketClient : TicketApiClient { - override fun getTicket(varCode: String): TicketApiResponseDto { - return TicketApiResponseDto(LocalDateTime.now().plusDays(10), BigDecimal.valueOf(20000)) - } - - override fun type(varCode: String): TicketType { - return TicketType.NOL - } -} diff --git a/src/main/kotlin/org/example/ticket/infra/api/TicketApiClient.kt b/src/main/kotlin/org/example/ticket/infra/api/TicketApiClient.kt deleted file mode 100644 index 3e8e3c4..0000000 --- a/src/main/kotlin/org/example/ticket/infra/api/TicketApiClient.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.example.ticket.infra.api - -import org.example.ticket.domain.enumeration.TicketType -import org.example.ticket.infra.dto.TicketApiResponseDto - -interface TicketApiClient { - fun getTicket(varCode: String) : TicketApiResponseDto - fun type(varCode: String) : TicketType -} diff --git a/src/main/kotlin/org/example/ticket/infra/repository/TicketJpaRepository.kt b/src/main/kotlin/org/example/ticket/infra/repository/TicketJpaRepository.kt deleted file mode 100644 index 235c684..0000000 --- a/src/main/kotlin/org/example/ticket/infra/repository/TicketJpaRepository.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.example.ticket.infra.repository - -import org.example.ticket.domain.model.Ticket -import org.springframework.data.jpa.repository.JpaRepository - -interface TicketJpaRepository: JpaRepository { -} diff --git a/src/main/kotlin/org/example/ticket/infrastructure/api/MelonTicketApiClient.kt b/src/main/kotlin/org/example/ticket/infrastructure/api/MelonTicketApiClient.kt new file mode 100644 index 0000000..b7fd61e --- /dev/null +++ b/src/main/kotlin/org/example/ticket/infrastructure/api/MelonTicketApiClient.kt @@ -0,0 +1,17 @@ +package org.example.ticket.infrastructure.api + +import org.example.ticket.infrastructure.dto.TicketApiResponseDto +import org.springframework.stereotype.Component +import java.math.BigDecimal +import java.time.LocalDateTime + +@Component +class MelonTicketApiClient : TicketApiClient{ + override fun getTicket(barcode: String): TicketApiResponseDto { + return TicketApiResponseDto( + + performanceDate = LocalDateTime.now().plusDays(3), + price = BigDecimal.valueOf(10000) + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/infrastructure/api/NolTicketApiClient.kt b/src/main/kotlin/org/example/ticket/infrastructure/api/NolTicketApiClient.kt new file mode 100644 index 0000000..ed69ea7 --- /dev/null +++ b/src/main/kotlin/org/example/ticket/infrastructure/api/NolTicketApiClient.kt @@ -0,0 +1,16 @@ +package org.example.ticket.infrastructure.api + +import org.example.ticket.infrastructure.dto.TicketApiResponseDto +import org.springframework.stereotype.Component +import java.math.BigDecimal +import java.time.LocalDateTime + +@Component +class NolTicketApiClient : TicketApiClient { + override fun getTicket(barcode: String): TicketApiResponseDto { + return TicketApiResponseDto( + performanceDate = LocalDateTime.now().plusDays(5), + price = BigDecimal.valueOf(20000) + ) + } +} diff --git a/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClient.kt b/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClient.kt new file mode 100644 index 0000000..0fa617d --- /dev/null +++ b/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClient.kt @@ -0,0 +1,8 @@ +package org.example.ticket.infrastructure.api + +import org.example.ticket.infrastructure.dto.TicketApiResponseDto + +interface TicketApiClient { + fun getTicket(barcode: String): TicketApiResponseDto + +} \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/infra/dto/TicketApiResponseDto.kt b/src/main/kotlin/org/example/ticket/infrastructure/dto/TicketApiResponseDto.kt similarity index 55% rename from src/main/kotlin/org/example/ticket/infra/dto/TicketApiResponseDto.kt rename to src/main/kotlin/org/example/ticket/infrastructure/dto/TicketApiResponseDto.kt index 855c1af..97bb47b 100644 --- a/src/main/kotlin/org/example/ticket/infra/dto/TicketApiResponseDto.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/dto/TicketApiResponseDto.kt @@ -1,4 +1,4 @@ -package org.example.ticket.infra.dto +package org.example.ticket.infrastructure.dto import org.example.ticket.domain.model.Ticket import java.math.BigDecimal @@ -8,10 +8,12 @@ data class TicketApiResponseDto( val performanceDate: LocalDateTime, val price: BigDecimal ) { - fun toTicket(): Ticket { + fun toTicket(barcode: String): Ticket { return Ticket( - expirationAt = performanceDate, - originalPrice = price, + barcode = barcode, + eventDateTime = performanceDate, + originalPrice = price ) } } + diff --git a/src/main/kotlin/org/example/ticket/presentation/TicketController.kt b/src/main/kotlin/org/example/ticket/presentation/TicketController.kt deleted file mode 100644 index 4dff4a0..0000000 --- a/src/main/kotlin/org/example/ticket/presentation/TicketController.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.example.ticket.presentation - -import org.example.ticket.application.dto.TicketCreationDto -import org.example.ticket.application.service.TicketService -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RestController - -@RestController -class TicketController( - private val ticketService: TicketService -) { - @PostMapping("/tickets") - fun registerTicket( - @RequestBody ticketCreationDto: TicketCreationDto - ) { - ticketService.createTicket(ticketCreationDto) - } -} From 069485b1d75e278435ed5dd83b75912de252fc29 Mon Sep 17 00:00:00 2001 From: songhansol Date: Wed, 25 Feb 2026 18:43:08 +0900 Subject: [PATCH 02/22] =?UTF-8?q?TicketJpaRepository=20=EA=B3=B5=EB=B6=80(?= =?UTF-8?q?=EC=A3=BC=EC=84=9D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 1 + .../service/TicketService/TicketService.kt | 18 ++- .../example/ticket/domain/enum/TicketType.kt | 3 +- .../org/example/ticket/domain/model/Ticket.kt | 40 ++++-- .../api/MelonTicketApiClient.kt | 6 +- .../infrastructure/api/MolTicketApiClient.kt | 20 +++ .../infrastructure/api/NolTicketApiClient.kt | 6 +- .../infrastructure/api/TicketApiClient.kt | 2 + .../dto/TicketApiResponseDto.kt | 8 +- .../repository/TicketJpaRepository.kt | 122 ++++++++++++++++++ .../org/example/ticket/domain/TicketTest.kt | 35 ++++- 11 files changed, 233 insertions(+), 28 deletions(-) create mode 100644 src/main/kotlin/org/example/ticket/infrastructure/api/MolTicketApiClient.kt create mode 100644 src/main/kotlin/org/example/ticket/infrastructure/repository/TicketJpaRepository.kt diff --git a/build.gradle.kts b/build.gradle.kts index 6a91807..93b18cc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,7 @@ plugins { kotlin("jvm") version "2.2.21" kotlin("plugin.spring") version "2.2.21" + kotlin("plugin.jpa") version "2.2.21" id("org.springframework.boot") version "4.0.2" id("io.spring.dependency-management") version "1.1.7" id("com.google.devtools.ksp") version "2.3.4" apply false diff --git a/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt b/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt index be53671..187022c 100644 --- a/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt +++ b/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt @@ -2,14 +2,26 @@ package org.example.ticket.application.service.TicketService import org.example.ticket.application.dto.TicketCreationDto import org.example.ticket.domain.model.Ticket +import org.example.ticket.infra.repository.TicketJpaRepository +import org.example.ticket.infrastructure.api.TicketApiClient import org.springframework.stereotype.Service @Service class TicketService ( - + private val ticketApiClient: List, + private val ticketRepository: TicketJpaRepository ) { - fun createTicket(ticketCreatioNDto: TicketCreationDto){ - val ticketType = Ticket. + fun createTicket(ticketCreationDto: TicketCreationDto){ + val ticketType = Ticket.ticketTypeCheck(ticketCreationDto.barcode); + val apiClient = ticketApiClient.first { it.type(ticketCreationDto.barcode) == ticketType }; + + val ticketResponseDto = apiClient.getTicket(ticketCreationDto.barcode); + val ticket = ticketResponseDto.toTicket(ticketCreationDto.barcode, ticketType); + Ticket.ticketTimeCheck(ticket.expirationDateTime) + ticketRepository.save(ticket) }; +// fun applySellerOfferPrice() { + // TODO : 구현 +// }; } \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/domain/enum/TicketType.kt b/src/main/kotlin/org/example/ticket/domain/enum/TicketType.kt index 28d8e4b..a44ee3a 100644 --- a/src/main/kotlin/org/example/ticket/domain/enum/TicketType.kt +++ b/src/main/kotlin/org/example/ticket/domain/enum/TicketType.kt @@ -2,5 +2,6 @@ package org.example.ticket.domain.enum enum class TicketType { MELON, - NOL + NOL, + MOL } \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt index 2ac15bb..cfd1b99 100644 --- a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt +++ b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt @@ -1,38 +1,50 @@ package org.example.ticket.domain.model +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id import org.example.ticket.domain.enum.TicketStatus import org.example.ticket.domain.enum.TicketType import org.springframework.cglib.core.Local import java.math.BigDecimal import java.time.LocalDateTime +@Entity class Ticket( + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long? = null, // ? => DB에 넘기기전에 id를 안넣으려고 (자동생성) val barcode: String, - val eventDateTime: LocalDateTime, + val expirationDateTime: LocalDateTime, val originalPrice: BigDecimal, // BigDecimal => 10진수로 저장(소수점 계산을 위해) - val sellingPrice: BigDecimal? = null, + var sellingPrice: BigDecimal? = null, var ticketStatus: TicketStatus = TicketStatus.ON_SALE, - var ticketType: TicketType? = null + val ticketType: TicketType ) { companion object { + fun getActivationDeadLine(): LocalDateTime { + return LocalDateTime.now().minusHours(1); + }; + fun ticketBarcodeCheck(barcode: String) { require(barcode.length == 8) { "바코드는 8자리여야 합니다." } } - fun ticketTimeCheck(eventTime: LocalDateTime) { - val deadLine: LocalDateTime = eventTime.plusHours(3) - require(eventTime.isAfter(deadLine)) { - "공연 시작 3시간 전까지만 등록할 수 있습니다." + fun ticketTimeCheck(performanceDateTime: LocalDateTime) { + val now = LocalDateTime.now() + val deadLine: LocalDateTime = performanceDateTime.minusHours(1) + require(now.isAfter(deadLine)) { + "공연 시작 1시간 전까지만 등록할 수 있습니다." } } fun ticketTypeCheck(barcode: String): TicketType { - return if (barcode.all { it.isLetter() }) { - TicketType.MELON - } else { - TicketType.NOL + return when { + barcode.all { it.isLetter() } -> TicketType.MELON + barcode.all { it.isDigit() } -> TicketType.NOL + else -> TicketType.MOL } + } @@ -40,8 +52,10 @@ class Ticket( init { ticketBarcodeCheck(barcode); - ticketTimeCheck(eventDateTime); - ticketType = ticketTypeCheck(barcode); + } + + fun applySellerOfferPrice(offerPrice: BigDecimal) { + val isPerformanceDateTime = LocalDateTime.now().toLocalDate() == expirationDateTime.toLocalDate() } } diff --git a/src/main/kotlin/org/example/ticket/infrastructure/api/MelonTicketApiClient.kt b/src/main/kotlin/org/example/ticket/infrastructure/api/MelonTicketApiClient.kt index b7fd61e..368e8d6 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/api/MelonTicketApiClient.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/api/MelonTicketApiClient.kt @@ -1,5 +1,6 @@ package org.example.ticket.infrastructure.api +import org.example.ticket.domain.enum.TicketType import org.example.ticket.infrastructure.dto.TicketApiResponseDto import org.springframework.stereotype.Component import java.math.BigDecimal @@ -10,8 +11,11 @@ class MelonTicketApiClient : TicketApiClient{ override fun getTicket(barcode: String): TicketApiResponseDto { return TicketApiResponseDto( - performanceDate = LocalDateTime.now().plusDays(3), + performanceDateTime = LocalDateTime.now().plusDays(3), price = BigDecimal.valueOf(10000) ) } + override fun type(varCode: String): TicketType { + return TicketType.MELON + } } \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/infrastructure/api/MolTicketApiClient.kt b/src/main/kotlin/org/example/ticket/infrastructure/api/MolTicketApiClient.kt new file mode 100644 index 0000000..37dcaf2 --- /dev/null +++ b/src/main/kotlin/org/example/ticket/infrastructure/api/MolTicketApiClient.kt @@ -0,0 +1,20 @@ +package org.example.ticket.infrastructure.api + +import org.example.ticket.domain.enum.TicketType +import org.example.ticket.infrastructure.dto.TicketApiResponseDto +import org.springframework.stereotype.Component +import java.math.BigDecimal +import java.time.LocalDateTime + +@Component +class MolTicketApiClient: TicketApiClient { + override fun getTicket(barcode: String): TicketApiResponseDto { + return TicketApiResponseDto( + performanceDateTime = LocalDateTime.now().plusDays(5), + price = BigDecimal.valueOf(30000) + ) + } + override fun type(varCode: String): TicketType { + return TicketType.MELON + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/infrastructure/api/NolTicketApiClient.kt b/src/main/kotlin/org/example/ticket/infrastructure/api/NolTicketApiClient.kt index ed69ea7..0203053 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/api/NolTicketApiClient.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/api/NolTicketApiClient.kt @@ -1,5 +1,6 @@ package org.example.ticket.infrastructure.api +import org.example.ticket.domain.enum.TicketType import org.example.ticket.infrastructure.dto.TicketApiResponseDto import org.springframework.stereotype.Component import java.math.BigDecimal @@ -9,8 +10,11 @@ import java.time.LocalDateTime class NolTicketApiClient : TicketApiClient { override fun getTicket(barcode: String): TicketApiResponseDto { return TicketApiResponseDto( - performanceDate = LocalDateTime.now().plusDays(5), + performanceDateTime = LocalDateTime.now().plusDays(5), price = BigDecimal.valueOf(20000) ) } + override fun type(varCode: String): TicketType { + return TicketType.NOL + } } diff --git a/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClient.kt b/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClient.kt index 0fa617d..6bd7fcb 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClient.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClient.kt @@ -1,8 +1,10 @@ package org.example.ticket.infrastructure.api +import org.example.ticket.domain.enum.TicketType import org.example.ticket.infrastructure.dto.TicketApiResponseDto interface TicketApiClient { fun getTicket(barcode: String): TicketApiResponseDto + fun type(varCode: String) : TicketType } \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/infrastructure/dto/TicketApiResponseDto.kt b/src/main/kotlin/org/example/ticket/infrastructure/dto/TicketApiResponseDto.kt index 97bb47b..34933a9 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/dto/TicketApiResponseDto.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/dto/TicketApiResponseDto.kt @@ -1,17 +1,19 @@ package org.example.ticket.infrastructure.dto +import org.example.ticket.domain.enum.TicketType import org.example.ticket.domain.model.Ticket import java.math.BigDecimal import java.time.LocalDateTime data class TicketApiResponseDto( - val performanceDate: LocalDateTime, + val performanceDateTime: LocalDateTime, val price: BigDecimal ) { - fun toTicket(barcode: String): Ticket { + fun toTicket(barcode: String, ticketType: TicketType): Ticket { return Ticket( barcode = barcode, - eventDateTime = performanceDate, + ticketType = ticketType, + expirationDateTime = performanceDateTime, originalPrice = price ) } diff --git a/src/main/kotlin/org/example/ticket/infrastructure/repository/TicketJpaRepository.kt b/src/main/kotlin/org/example/ticket/infrastructure/repository/TicketJpaRepository.kt new file mode 100644 index 0000000..ea111e8 --- /dev/null +++ b/src/main/kotlin/org/example/ticket/infrastructure/repository/TicketJpaRepository.kt @@ -0,0 +1,122 @@ +package org.example.ticket.infra.repository + +import org.example.ticket.domain.model.Ticket +import org.springframework.data.jpa.repository.JpaRepository + +interface TicketJpaRepository: JpaRepository { +} + +/* JPA 동작 방식 + 스프링이 JPA 의존성을 보고 어떻게 이 라이브러리코드가 동작하도록 끌어오는가? + 1. 의존성에 JPA가 존재하는걸 확인 + 2. JpaRepository 상속한 인터페이스를 찾음 + 3. 런타임 메모리에 CRUD 메서드가 선언된 클래스 자동 생성 + 4. 생성된 클래스 자바빈 등록 + 5. JpaRepository를 상속받은 클래스(작성한 클래스)를 통해 CRUD가능 +*/ + +/* 이해 안가던 점 + 1. Repository를 CrudRepository가 확장 + 2. CrudRepository를 ListCrudRepository가 확장 + 3. JpaRepository가 ListCrudRepository를 확장 + + A : JpaRepository -> TicketJpaRepository + B : JpaRepository -> JpaRepositoryImplementation -> SimpleJpaRepository + + SimpleRepository(라이브러리)/TicketJpaRepository(내구현) + + 1. TicketJpaRepository.save를 타고올라가면 CrudRepository.save가 나옴 + 2. CrudRepository.save를 구현한건 SimpleJpaRepository임 + 3. ? 그런데 우리가 쓰는건 TicketJpaRepository인데 SimpleJpaRepository.save가 어떻게 연결되지? + (TicketJpaRepository.save는 선언안했는데?) + + => 스프링이 프록시를 자동 생성해주기 때문 + 최상위 부터 동작을 추적하면 + + 1. Spring Boot Auto Configuration + => "JPA 의존성 있네? @EnableJpaRepositories 자동 활성화" + + 2. JpaRepositoriesRegistrar + => "JpaRepository 상속한 인터페이스 스캔해" → TicketJpaRepository 발견 + + 3. JpaRepositoryFactoryBean (인터페이스당 하나 생성) + => "TicketJpaRepository를 처리할 팩토리 만들자" + + 4. JpaRepositoryFactory + => SimpleJpaRepository 생성 + => 프록시로 감싸기 + => Bean 등록 + + => 결론 : TicketJpaRepository.save가 SimpleJpaRepository.save로 호출되는건 단순하게 스프링 내부 동작임 + SimpleJpaRepository에 보면 라고 선언되어 있는데 + * Default implementation of the {@link org.springframework.data.repository.CrudRepository} interface. This will offer + * you a more sophisticated interface than the plain {@link EntityManager} . + 이렇게 SimpleJpaRepository에가 CrudRepository의 기본 구현체라고 기입되어 있는데, + => 프록시가 자동으로 연결해준단 말임 + => extends(확장/상속)가 아니라 delegation(위임)하는 형태 + => 이해하기 어려운 부분은 작성하는 코드에 보이는게 아니라 런타임 구현체를 스프링이 생성한다고 보는것 + => 본인 코드만 보면 이 동작을 절대 알 수 없음 => Spring Doc를 읽읍시다 + +*/ + + +/* +JPA 내부동작 +JPA의 핵심은 "객체와 DB 테이블을 매핑해서, SQL을 직접 안 짜고 객체로 DB를 다루게 해주는 것" +TicketJpaRepository → JpaRepository → ListCrudRepository → CrudRepository +전부 인터페이스 → 껍데기만 있음 → save()의 실제 코드가 없음 +Spring이 해주는 건 이 껍데기에 SimpleJpaRepository라는 구현체를 연결해주는 것입니다. + 1. 매핑 (앱 시작 시) + Ticket 클래스를 분석 (리플렉션) + → @Entity 확인 → "이건 DB 테이블이구나" + → 클래스명 Ticket → 테이블명 ticket + → 필드 barcode: String → 컬럼 barcode VARCHAR + → 필드 originalPrice: BigDecimal → 컬럼 original_price DECIMAL + → 필드 id: Long + @Id → Primary Key + + 결과: 객체 ↔ 테이블 매핑 정보를 메모리에 보관 + + 2. 영속성 컨텍스트 (핵심) + + JPA는 객체와 DB 사이에 중간 저장소를 둡니다: + + 우리 코드 ↔ 영속성 컨텍스트(메모리) ↔ DB + + 모든 동작이 이 영속성 컨텍스트를 거칩니다: + + save(ticket) → 영속성 컨텍스트에 등록 → 커밋 시 INSERT + findById(1L) → 영속성 컨텍스트에 있으면 그거 반환 (DB 안 감) + → 없으면 DB 조회 → 영속성 컨텍스트에 저장 → 반환 + delete(ticket) → 영속성 컨텍스트에서 삭제 표시 → 커밋 시 DELETE + + 3. 변경 감지 (Dirty Checking) + + 이게 JPA의 가장 강력한 기능입니다: + + val ticket = ticketRepository.findById(1L) // DB에서 조회 + ticket.ticketStatus = TicketStatus.SOLD // 필드만 바꿈 + // save() 안 해도 됨! + // 트랜잭션 커밋 시 JPA가 "어? 값이 바뀌었네" → UPDATE 자동 실행 + + 조회 시: 원본 스냅샷을 저장해둠 {status: ON_SALE} + 커밋 시: 현재 상태와 스냅샷 비교 {status: SOLD} vs {status: ON_SALE} + → "status 바뀌었네" → UPDATE ticket SET status = 'SOLD' WHERE id = 1 + + 4. 전체 흐름 정리 + + 트랜잭션 시작 + ↓ + 조회/저장/수정/삭제 → 전부 영속성 컨텍스트에서 처리 + ↓ + 트랜잭션 커밋 + ↓ + 영속성 컨텍스트와 DB의 차이를 비교 + ↓ + 필요한 SQL만 생성해서 DB에 실행 (INSERT, UPDATE, DELETE) + ↓ + 영속성 컨텍스트 초기화 + + 결국 JPA는 **"DB를 직접 건드리지 말고, 객체만 다뤄. 나머지는 내가 알아서 SQL로 바꿔줄게"**가 전부입니다. + 영속성 컨텍스트가 그 중간다리 역할을 하는 거고요. + + */ \ No newline at end of file diff --git a/src/test/kotlin/org/example/ticket/domain/TicketTest.kt b/src/test/kotlin/org/example/ticket/domain/TicketTest.kt index 55dee9c..d9838a9 100644 --- a/src/test/kotlin/org/example/ticket/domain/TicketTest.kt +++ b/src/test/kotlin/org/example/ticket/domain/TicketTest.kt @@ -2,6 +2,7 @@ package org.example.ticket.domain import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.SoftAssertions.assertSoftly +import org.example.ticket.domain.enum.TicketType import org.example.ticket.domain.model.Ticket import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows @@ -11,11 +12,17 @@ import java.time.LocalDateTime class TicketTest { @Test fun `티켓_만들기`() { - val ticket = Ticket(1L, LocalDateTime.now(), BigDecimal.TEN) + val ticket = Ticket( + id = 1L, + barcode = "ABCDEFGH", + expirationDateTime = LocalDateTime.now().plusDays(1), + originalPrice = BigDecimal.TEN, + ticketType = TicketType.MELON + ) assertSoftly { assertThat(ticket.id).isEqualTo(1L) - assertThat(ticket.expirationAt).isNotNull + assertThat(ticket.expirationDateTime).isNotNull assertThat(ticket.originalPrice).isEqualTo(BigDecimal.TEN) } } @@ -23,14 +30,25 @@ class TicketTest { @Test fun `티켓 유효기간이 등록 기준 시간보다 늦으면 예외이다`() { assertThrows { - Ticket(expirationAt = Ticket.ACTIVATION_DEAD_LINE.minusSeconds(1), originalPrice = BigDecimal.TEN) + Ticket( + barcode = "ABCDEFGH", + expirationDateTime = Ticket.getActivationDeadLine().minusSeconds(1), + originalPrice = BigDecimal.TEN, + ticketType = TicketType.MELON + ) } } @Test fun `공연 당일에는 가격을 정가의 50% 이하로만 설정 할 수 있다`() { val ticketPrice = BigDecimal.TEN - val ticket = Ticket(1L, Ticket.ACTIVATION_DEAD_LINE.plusSeconds(1), ticketPrice) + val ticket = Ticket( + id = 1L, + barcode = "ABCDEFGH", + expirationDateTime = Ticket.getActivationDeadLine().plusSeconds(1), + originalPrice = ticketPrice, + ticketType = TicketType.MELON + ) assertThrows { ticket.applySellerOfferPrice(BigDecimal.valueOf(100.0)) @@ -39,9 +57,14 @@ class TicketTest { @Test fun `양도 가격은 정가를 초과 할 수 없다`() { - // API assertThrows { - Ticket(1L, LocalDateTime.now(), BigDecimal.TEN) + Ticket( + id = 1L, + barcode = "ABCDEFGH", + expirationDateTime = LocalDateTime.now().plusDays(1), + originalPrice = BigDecimal.TEN, + ticketType = TicketType.MELON + ) } } } From 596fdcb817d818c5750cd41169d8fc3efc2eccde Mon Sep 17 00:00:00 2001 From: songhansol Date: Wed, 25 Feb 2026 18:54:09 +0900 Subject: [PATCH 03/22] =?UTF-8?q?Ticket=20=EC=9C=A0=ED=9A=A8=EA=B8=B0?= =?UTF-8?q?=EA=B0=84=20=EC=B2=B4=ED=81=AC=20init=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/TicketService/TicketService.kt | 7 ++++--- src/main/kotlin/org/example/ticket/domain/model/Ticket.kt | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt b/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt index 187022c..71a1d3a 100644 --- a/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt +++ b/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt @@ -17,11 +17,12 @@ class TicketService ( val ticketResponseDto = apiClient.getTicket(ticketCreationDto.barcode); val ticket = ticketResponseDto.toTicket(ticketCreationDto.barcode, ticketType); - Ticket.ticketTimeCheck(ticket.expirationDateTime) ticketRepository.save(ticket) }; -// fun applySellerOfferPrice() { + fun applySellerOfferPrice(barcode: String) { // TODO : 구현 -// }; + + + }; } \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt index cfd1b99..0d7b564 100644 --- a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt +++ b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt @@ -52,6 +52,8 @@ class Ticket( init { ticketBarcodeCheck(barcode); + ticketTimeCheck(expirationDateTime); + } fun applySellerOfferPrice(offerPrice: BigDecimal) { From 1f7205d0f991bc0816caa667a19b505d7f3f2277 Mon Sep 17 00:00:00 2001 From: songhansol Date: Thu, 26 Feb 2026 10:10:52 +0900 Subject: [PATCH 04/22] =?UTF-8?q?applySellerOfferPrice=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/TicketService/TicketService.kt | 12 +++++++----- .../org/example/ticket/domain/model/Ticket.kt | 17 +++++++++++++---- .../api/TicketApiClientResolver.kt | 12 ++++++++++++ .../repository/TicketJpaRepository.kt | 1 + .../org/example/ticket/domain/TicketTest.kt | 2 +- 5 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClientResolver.kt diff --git a/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt b/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt index 71a1d3a..858457d 100644 --- a/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt +++ b/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt @@ -5,6 +5,7 @@ import org.example.ticket.domain.model.Ticket import org.example.ticket.infra.repository.TicketJpaRepository import org.example.ticket.infrastructure.api.TicketApiClient import org.springframework.stereotype.Service +import java.math.BigDecimal @Service class TicketService ( @@ -14,15 +15,16 @@ class TicketService ( fun createTicket(ticketCreationDto: TicketCreationDto){ val ticketType = Ticket.ticketTypeCheck(ticketCreationDto.barcode); val apiClient = ticketApiClient.first { it.type(ticketCreationDto.barcode) == ticketType }; - val ticketResponseDto = apiClient.getTicket(ticketCreationDto.barcode); val ticket = ticketResponseDto.toTicket(ticketCreationDto.barcode, ticketType); ticketRepository.save(ticket) }; - fun applySellerOfferPrice(barcode: String) { - // TODO : 구현 - - }; + fun applySellerOfferPrice(barcode: String, offerPrice: BigDecimal) { + val ticket = ticketRepository.findByBarcode(barcode) + requireNotNull(ticket){"해당 티켓 정보가 존재하지 않습니다."} + ticket.applySellerOfferPrice(offerPrice) + ticketRepository.save(ticket) + } } \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt index 0d7b564..8a7c95b 100644 --- a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt +++ b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt @@ -22,6 +22,8 @@ class Ticket( val ticketType: TicketType ) { companion object { + private val HALF = BigDecimal("0.5") + fun getActivationDeadLine(): LocalDateTime { return LocalDateTime.now().minusHours(1); }; @@ -33,7 +35,7 @@ class Ticket( fun ticketTimeCheck(performanceDateTime: LocalDateTime) { val now = LocalDateTime.now() val deadLine: LocalDateTime = performanceDateTime.minusHours(1) - require(now.isAfter(deadLine)) { + require(now.isBefore(deadLine)) { "공연 시작 1시간 전까지만 등록할 수 있습니다." } } @@ -51,13 +53,20 @@ class Ticket( } init { - ticketBarcodeCheck(barcode); - ticketTimeCheck(expirationDateTime); - + ticketBarcodeCheck(barcode) + ticketTimeCheck(expirationDateTime) } fun applySellerOfferPrice(offerPrice: BigDecimal) { + require(offerPrice >= BigDecimal.ZERO) { "판매가격에 음수는 입력할 수 없습니다." } + require(offerPrice <= originalPrice) { "티켓의 가격은 정가를 초과할 수 없습니다." } val isPerformanceDateTime = LocalDateTime.now().toLocalDate() == expirationDateTime.toLocalDate() + if (isPerformanceDateTime) { + val maxAllowedPrice = originalPrice.multiply(HALF) + require(offerPrice <= maxAllowedPrice) { "공연 당일에는 가격을 정가의 50% 이하로만 설정할 수 있습니다." } + } + + sellingPrice = offerPrice } } diff --git a/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClientResolver.kt b/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClientResolver.kt new file mode 100644 index 0000000..c177d62 --- /dev/null +++ b/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClientResolver.kt @@ -0,0 +1,12 @@ +package org.example.ticket.infrastructure.api + +import org.example.ticket.domain.model.Ticket + +class TicketApiClientResolver ( + private val ticketApiClients: List +){ + fun resolve(barcode: String): TicketApiClient { + val ticketType = Ticket.ticketTypeCheck(barcode) + return ticketApiClients.first { it.type(barcode) == ticketType } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/infrastructure/repository/TicketJpaRepository.kt b/src/main/kotlin/org/example/ticket/infrastructure/repository/TicketJpaRepository.kt index ea111e8..6284124 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/repository/TicketJpaRepository.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/repository/TicketJpaRepository.kt @@ -4,6 +4,7 @@ import org.example.ticket.domain.model.Ticket import org.springframework.data.jpa.repository.JpaRepository interface TicketJpaRepository: JpaRepository { + fun findByBarcode(barcode: String): Ticket? // ? 넣어야 null 처리되서 조회 안될때 에러가 안남 } /* JPA 동작 방식 diff --git a/src/test/kotlin/org/example/ticket/domain/TicketTest.kt b/src/test/kotlin/org/example/ticket/domain/TicketTest.kt index d9838a9..e19ab4d 100644 --- a/src/test/kotlin/org/example/ticket/domain/TicketTest.kt +++ b/src/test/kotlin/org/example/ticket/domain/TicketTest.kt @@ -15,7 +15,7 @@ class TicketTest { val ticket = Ticket( id = 1L, barcode = "ABCDEFGH", - expirationDateTime = LocalDateTime.now().plusDays(1), + expirationDateTime = LocalDateTime.now().plusHours(1), originalPrice = BigDecimal.TEN, ticketType = TicketType.MELON ) From c51a6640656211bd53add871e47125e1fdd77de5 Mon Sep 17 00:00:00 2001 From: songhansol Date: Thu, 26 Feb 2026 10:14:08 +0900 Subject: [PATCH 05/22] =?UTF-8?q?=EA=B0=81=20ApiClient=EC=97=90=20type=20?= =?UTF-8?q?=EB=B0=94=EC=BD=94=EB=93=9C=20=EC=9D=B8=EC=9E=90=20=ED=95=84?= =?UTF-8?q?=EC=9A=94=EC=97=86=EC=96=B4=EC=84=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/ticket/infrastructure/api/MelonTicketApiClient.kt | 2 +- .../org/example/ticket/infrastructure/api/MolTicketApiClient.kt | 2 +- .../org/example/ticket/infrastructure/api/NolTicketApiClient.kt | 2 +- .../org/example/ticket/infrastructure/api/TicketApiClient.kt | 2 +- .../ticket/infrastructure/api/TicketApiClientResolver.kt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/org/example/ticket/infrastructure/api/MelonTicketApiClient.kt b/src/main/kotlin/org/example/ticket/infrastructure/api/MelonTicketApiClient.kt index 368e8d6..df61c4c 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/api/MelonTicketApiClient.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/api/MelonTicketApiClient.kt @@ -15,7 +15,7 @@ class MelonTicketApiClient : TicketApiClient{ price = BigDecimal.valueOf(10000) ) } - override fun type(varCode: String): TicketType { + override fun type(): TicketType { return TicketType.MELON } } \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/infrastructure/api/MolTicketApiClient.kt b/src/main/kotlin/org/example/ticket/infrastructure/api/MolTicketApiClient.kt index 37dcaf2..475d850 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/api/MolTicketApiClient.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/api/MolTicketApiClient.kt @@ -14,7 +14,7 @@ class MolTicketApiClient: TicketApiClient { price = BigDecimal.valueOf(30000) ) } - override fun type(varCode: String): TicketType { + override fun type(): TicketType { return TicketType.MELON } } \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/infrastructure/api/NolTicketApiClient.kt b/src/main/kotlin/org/example/ticket/infrastructure/api/NolTicketApiClient.kt index 0203053..0508503 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/api/NolTicketApiClient.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/api/NolTicketApiClient.kt @@ -14,7 +14,7 @@ class NolTicketApiClient : TicketApiClient { price = BigDecimal.valueOf(20000) ) } - override fun type(varCode: String): TicketType { + override fun type(): TicketType { return TicketType.NOL } } diff --git a/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClient.kt b/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClient.kt index 6bd7fcb..ae56389 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClient.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClient.kt @@ -5,6 +5,6 @@ import org.example.ticket.infrastructure.dto.TicketApiResponseDto interface TicketApiClient { fun getTicket(barcode: String): TicketApiResponseDto - fun type(varCode: String) : TicketType + fun type() : TicketType } \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClientResolver.kt b/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClientResolver.kt index c177d62..1e65643 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClientResolver.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClientResolver.kt @@ -7,6 +7,6 @@ class TicketApiClientResolver ( ){ fun resolve(barcode: String): TicketApiClient { val ticketType = Ticket.ticketTypeCheck(barcode) - return ticketApiClients.first { it.type(barcode) == ticketType } + return ticketApiClients.first { it.type() == ticketType } } } \ No newline at end of file From 2f646688f1c8ee9e9eca3a376c268bc80b16edf6 Mon Sep 17 00:00:00 2001 From: songhansol Date: Thu, 26 Feb 2026 10:21:23 +0900 Subject: [PATCH 06/22] =?UTF-8?q?ApiClient=20=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81TicketApiClientResolver=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/TicketService/TicketService.kt | 10 +++++----- .../infrastructure/api/TicketApiClientResolver.kt | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt b/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt index 858457d..5554f12 100644 --- a/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt +++ b/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt @@ -4,19 +4,19 @@ import org.example.ticket.application.dto.TicketCreationDto import org.example.ticket.domain.model.Ticket import org.example.ticket.infra.repository.TicketJpaRepository import org.example.ticket.infrastructure.api.TicketApiClient +import org.example.ticket.infrastructure.api.TicketApiClientResolver import org.springframework.stereotype.Service import java.math.BigDecimal @Service class TicketService ( - private val ticketApiClient: List, + private val ticketApiClientResolver: TicketApiClientResolver, private val ticketRepository: TicketJpaRepository ) { fun createTicket(ticketCreationDto: TicketCreationDto){ - val ticketType = Ticket.ticketTypeCheck(ticketCreationDto.barcode); - val apiClient = ticketApiClient.first { it.type(ticketCreationDto.barcode) == ticketType }; - val ticketResponseDto = apiClient.getTicket(ticketCreationDto.barcode); - val ticket = ticketResponseDto.toTicket(ticketCreationDto.barcode, ticketType); + val apiClient = ticketApiClientResolver.resolve(ticketCreationDto.barcode) + val ticketResponseDto = apiClient.getTicket(ticketCreationDto.barcode) + val ticket = ticketResponseDto.toTicket(ticketCreationDto.barcode, apiClient.type()) ticketRepository.save(ticket) }; diff --git a/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClientResolver.kt b/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClientResolver.kt index 1e65643..c01cad6 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClientResolver.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClientResolver.kt @@ -1,7 +1,9 @@ package org.example.ticket.infrastructure.api import org.example.ticket.domain.model.Ticket +import org.springframework.stereotype.Component +@Component class TicketApiClientResolver ( private val ticketApiClients: List ){ From d6db88ceb45b28924f2fce5ad0d39a245d7afd15 Mon Sep 17 00:00:00 2001 From: songhansol Date: Thu, 26 Feb 2026 11:40:32 +0900 Subject: [PATCH 07/22] =?UTF-8?q?1.=20=ED=8B=B0=EC=BC=93=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=EC=97=90=20SellerName=EC=B6=94=EA=B0=80=202.=20?= =?UTF-8?q?=EA=B0=84=EB=8B=A8=ED=95=9C=20=EC=9C=A0=EC=A0=80=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=B6=94=EA=B0=80=203.=20=ED=8B=B0?= =?UTF-8?q?=EC=BC=93=EC=83=9D=EC=84=B1=20TicketCreationDto=EC=97=90=20?= =?UTF-8?q?=ED=8C=90=EB=A7=A4=EC=9E=90=EB=AA=85=20=EC=B6=94=EA=B0=80=204.?= =?UTF-8?q?=20=EA=B0=80=EA=B2=A9=EC=88=98=EC=A0=95=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20=EC=B6=94=EA=B0=80(=EC=9C=A0=EC=A0=80=ED=99=95?= =?UTF-8?q?=EC=9D=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ticket/application/dto/TicketCreationDto.kt | 1 + .../service/TicketService/TicketService.kt | 11 ++++++++--- .../org/example/ticket/domain/model/Ticket.kt | 4 +++- .../infrastructure/dto/TicketApiResponseDto.kt | 3 ++- .../org/example/user/domain/enum/UserRole.kt | 6 ++++++ .../kotlin/org/example/user/domain/model/User.kt | 12 ++++++++++++ .../org/example/user/repository/UserRepository.kt | 15 +++++++++++++++ 7 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/org/example/user/domain/enum/UserRole.kt create mode 100644 src/main/kotlin/org/example/user/domain/model/User.kt create mode 100644 src/main/kotlin/org/example/user/repository/UserRepository.kt diff --git a/src/main/kotlin/org/example/ticket/application/dto/TicketCreationDto.kt b/src/main/kotlin/org/example/ticket/application/dto/TicketCreationDto.kt index 1f66070..34114ba 100644 --- a/src/main/kotlin/org/example/ticket/application/dto/TicketCreationDto.kt +++ b/src/main/kotlin/org/example/ticket/application/dto/TicketCreationDto.kt @@ -1,5 +1,6 @@ package org.example.ticket.application.dto data class TicketCreationDto( + val sellerName: String, val barcode: String ) diff --git a/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt b/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt index 5554f12..affd8ab 100644 --- a/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt +++ b/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt @@ -16,15 +16,20 @@ class TicketService ( fun createTicket(ticketCreationDto: TicketCreationDto){ val apiClient = ticketApiClientResolver.resolve(ticketCreationDto.barcode) val ticketResponseDto = apiClient.getTicket(ticketCreationDto.barcode) - val ticket = ticketResponseDto.toTicket(ticketCreationDto.barcode, apiClient.type()) + val ticket = ticketResponseDto.toTicket( + ticketCreationDto.barcode, + ticketCreationDto.sellerName, + apiClient.type(), + ) ticketRepository.save(ticket) }; - fun applySellerOfferPrice(barcode: String, offerPrice: BigDecimal) { + fun applySellerOfferPrice(offerSellerName: String, barcode: String, offerPrice: BigDecimal) { val ticket = ticketRepository.findByBarcode(barcode) requireNotNull(ticket){"해당 티켓 정보가 존재하지 않습니다."} - ticket.applySellerOfferPrice(offerPrice) + + ticket.applySellerOfferPrice(offerSellerName, offerPrice); ticketRepository.save(ticket) } } \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt index 8a7c95b..d8d8caa 100644 --- a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt +++ b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt @@ -17,6 +17,7 @@ class Ticket( val barcode: String, val expirationDateTime: LocalDateTime, val originalPrice: BigDecimal, // BigDecimal => 10진수로 저장(소수점 계산을 위해) + val sellerName: String, var sellingPrice: BigDecimal? = null, var ticketStatus: TicketStatus = TicketStatus.ON_SALE, val ticketType: TicketType @@ -57,7 +58,8 @@ class Ticket( ticketTimeCheck(expirationDateTime) } - fun applySellerOfferPrice(offerPrice: BigDecimal) { + fun applySellerOfferPrice(offerSellerName: String, offerPrice: BigDecimal) { + require(sellerName == offerSellerName){"티켓에 등록된 판매자가 아닙니다."} require(offerPrice >= BigDecimal.ZERO) { "판매가격에 음수는 입력할 수 없습니다." } require(offerPrice <= originalPrice) { "티켓의 가격은 정가를 초과할 수 없습니다." } val isPerformanceDateTime = LocalDateTime.now().toLocalDate() == expirationDateTime.toLocalDate() diff --git a/src/main/kotlin/org/example/ticket/infrastructure/dto/TicketApiResponseDto.kt b/src/main/kotlin/org/example/ticket/infrastructure/dto/TicketApiResponseDto.kt index 34933a9..9a7c2d8 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/dto/TicketApiResponseDto.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/dto/TicketApiResponseDto.kt @@ -9,10 +9,11 @@ data class TicketApiResponseDto( val performanceDateTime: LocalDateTime, val price: BigDecimal ) { - fun toTicket(barcode: String, ticketType: TicketType): Ticket { + fun toTicket(barcode: String, sellerId: String, ticketType: TicketType,): Ticket { return Ticket( barcode = barcode, ticketType = ticketType, + sellerId = sellerId, expirationDateTime = performanceDateTime, originalPrice = price ) diff --git a/src/main/kotlin/org/example/user/domain/enum/UserRole.kt b/src/main/kotlin/org/example/user/domain/enum/UserRole.kt new file mode 100644 index 0000000..e97da0e --- /dev/null +++ b/src/main/kotlin/org/example/user/domain/enum/UserRole.kt @@ -0,0 +1,6 @@ +package org.example.user.domain.enum + +enum class UserRole { + SELLER, + BUYER, +} \ No newline at end of file diff --git a/src/main/kotlin/org/example/user/domain/model/User.kt b/src/main/kotlin/org/example/user/domain/model/User.kt new file mode 100644 index 0000000..2ea6e3c --- /dev/null +++ b/src/main/kotlin/org/example/user/domain/model/User.kt @@ -0,0 +1,12 @@ +package org.example.user.domain.model + + +import org.example.user.domain.enum.UserRole +class User ( + val id: Long? = null, + val name : String, + val role : UserRole +){ + companion object{} + init {} +} \ No newline at end of file diff --git a/src/main/kotlin/org/example/user/repository/UserRepository.kt b/src/main/kotlin/org/example/user/repository/UserRepository.kt new file mode 100644 index 0000000..b11be7b --- /dev/null +++ b/src/main/kotlin/org/example/user/repository/UserRepository.kt @@ -0,0 +1,15 @@ +package org.example.user.repository + +import org.example.user.domain.enum.UserRole +import org.example.user.domain.model.User + +object UserRepository { + private val users = listOf( + User(id = 1, name = "판매자1", role = UserRole.SELLER), + User(id = 2, name = "판매자2", role = UserRole.SELLER), + User(id = 3, name = "구매자1", role = UserRole.BUYER), + User(id = 4, name = "구매자2", role = UserRole.BUYER), + ) + + fun findByName(name: String): User? = users.find { it.name == name } +} \ No newline at end of file From de786898de6f67331743a968e94e57084220a402 Mon Sep 17 00:00:00 2001 From: songhansol Date: Thu, 26 Feb 2026 12:03:15 +0900 Subject: [PATCH 08/22] =?UTF-8?q?getTicketDeadLine=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=ED=8B=B0=EC=BC=93=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/example/ticket/domain/model/Ticket.kt | 51 +++++++++++++------ .../dto/TicketApiResponseDto.kt | 2 +- .../org/example/ticket/domain/TicketTest.kt | 10 ++-- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt index d8d8caa..80092e5 100644 --- a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt +++ b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt @@ -25,22 +25,10 @@ class Ticket( companion object { private val HALF = BigDecimal("0.5") - fun getActivationDeadLine(): LocalDateTime { - return LocalDateTime.now().minusHours(1); - }; - fun ticketBarcodeCheck(barcode: String) { require(barcode.length == 8) { "바코드는 8자리여야 합니다." } } - fun ticketTimeCheck(performanceDateTime: LocalDateTime) { - val now = LocalDateTime.now() - val deadLine: LocalDateTime = performanceDateTime.minusHours(1) - require(now.isBefore(deadLine)) { - "공연 시작 1시간 전까지만 등록할 수 있습니다." - } - } - fun ticketTypeCheck(barcode: String): TicketType { return when { barcode.all { it.isLetter() } -> TicketType.MELON @@ -49,8 +37,6 @@ class Ticket( } } - - } init { @@ -58,8 +44,20 @@ class Ticket( ticketTimeCheck(expirationDateTime) } + fun getTicketDeadLine(): LocalDateTime { + return expirationDateTime.minusHours(1); + }; + + fun ticketTimeCheck(performanceDateTime: LocalDateTime) { + val now = LocalDateTime.now() + val deadLine: LocalDateTime = getTicketDeadLine(); + require(now.isBefore(deadLine)) { + "공연 시작 1시간 전까지만 등록할 수 있습니다." + } + } + fun applySellerOfferPrice(offerSellerName: String, offerPrice: BigDecimal) { - require(sellerName == offerSellerName){"티켓에 등록된 판매자가 아닙니다."} + require(sellerName == offerSellerName) { "티켓에 등록된 판매자가 아닙니다." } require(offerPrice >= BigDecimal.ZERO) { "판매가격에 음수는 입력할 수 없습니다." } require(offerPrice <= originalPrice) { "티켓의 가격은 정가를 초과할 수 없습니다." } val isPerformanceDateTime = LocalDateTime.now().toLocalDate() == expirationDateTime.toLocalDate() @@ -71,6 +69,29 @@ class Ticket( sellingPrice = offerPrice } + fun ticketOnSale() { + require(ticketStatus == TicketStatus.RESERVED) { "예약취소된 티켓만 재판매 할 수 있습니다." } + ticketStatus = TicketStatus.ON_SALE + } + + fun ticketReserve() { + require(ticketStatus == TicketStatus.ON_SALE) { "예약중인 티켓만 판매완료할 수 있습니다." } + ticketStatus = TicketStatus.RESERVED + } + + fun ticketSold() { + require(ticketStatus == TicketStatus.RESERVED) { "예약중인 티켓만 판매완료할 수 있습니다." } + ticketStatus = TicketStatus.SOLD + } + + fun ticketExpired() { + require(ticketStatus != TicketStatus.SOLD) { "판매완료된 티켓은 만료할 수 없습니다." } + val now = LocalDateTime.now() + val deadLine = getTicketDeadLine() + require(now.isAfter(deadLine)) { "만료되지 않은 티켓입니다." } + ticketStatus = TicketStatus.EXPIRED + } + } diff --git a/src/main/kotlin/org/example/ticket/infrastructure/dto/TicketApiResponseDto.kt b/src/main/kotlin/org/example/ticket/infrastructure/dto/TicketApiResponseDto.kt index 9a7c2d8..8bdbecf 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/dto/TicketApiResponseDto.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/dto/TicketApiResponseDto.kt @@ -13,7 +13,7 @@ data class TicketApiResponseDto( return Ticket( barcode = barcode, ticketType = ticketType, - sellerId = sellerId, + sellerName = sellerId, expirationDateTime = performanceDateTime, originalPrice = price ) diff --git a/src/test/kotlin/org/example/ticket/domain/TicketTest.kt b/src/test/kotlin/org/example/ticket/domain/TicketTest.kt index e19ab4d..737806d 100644 --- a/src/test/kotlin/org/example/ticket/domain/TicketTest.kt +++ b/src/test/kotlin/org/example/ticket/domain/TicketTest.kt @@ -15,6 +15,7 @@ class TicketTest { val ticket = Ticket( id = 1L, barcode = "ABCDEFGH", + sellerName = "Seller A", expirationDateTime = LocalDateTime.now().plusHours(1), originalPrice = BigDecimal.TEN, ticketType = TicketType.MELON @@ -32,7 +33,8 @@ class TicketTest { assertThrows { Ticket( barcode = "ABCDEFGH", - expirationDateTime = Ticket.getActivationDeadLine().minusSeconds(1), + sellerName = "Seller A", + expirationDateTime = Ticket.getTicketDeadLine().minusSeconds(1), originalPrice = BigDecimal.TEN, ticketType = TicketType.MELON ) @@ -45,13 +47,14 @@ class TicketTest { val ticket = Ticket( id = 1L, barcode = "ABCDEFGH", - expirationDateTime = Ticket.getActivationDeadLine().plusSeconds(1), + sellerName = "Seller A", + expirationDateTime = Ticket.getTicketDeadLine().plusSeconds(1), originalPrice = ticketPrice, ticketType = TicketType.MELON ) assertThrows { - ticket.applySellerOfferPrice(BigDecimal.valueOf(100.0)) + ticket.applySellerOfferPrice("seller_A", BigDecimal.valueOf(100.0)) } } @@ -61,6 +64,7 @@ class TicketTest { Ticket( id = 1L, barcode = "ABCDEFGH", + sellerName = "Seller A", expirationDateTime = LocalDateTime.now().plusDays(1), originalPrice = BigDecimal.TEN, ticketType = TicketType.MELON From 275d7164fa3986a2c417dca8290ee607854b348b Mon Sep 17 00:00:00 2001 From: songhansol Date: Thu, 26 Feb 2026 17:29:10 +0900 Subject: [PATCH 09/22] =?UTF-8?q?deal=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1,=20=EA=B1=B0=EB=9E=98=EC=8B=9C=EC=9E=91(RESE?= =?UTF-8?q?RVED)=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=A7=8C=EB=93=A4?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deal/application/service/DealService.kt | 15 +++++++++ .../example/deal/domain/enum/DealStatus.kt | 7 +++++ .../org/example/deal/domain/model/Deal.kt | 31 +++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 src/main/kotlin/org/example/deal/application/service/DealService.kt create mode 100644 src/main/kotlin/org/example/deal/domain/enum/DealStatus.kt create mode 100644 src/main/kotlin/org/example/deal/domain/model/Deal.kt diff --git a/src/main/kotlin/org/example/deal/application/service/DealService.kt b/src/main/kotlin/org/example/deal/application/service/DealService.kt new file mode 100644 index 0000000..7d2ca39 --- /dev/null +++ b/src/main/kotlin/org/example/deal/application/service/DealService.kt @@ -0,0 +1,15 @@ +package org.example.deal.application.service + +import org.example.deal.domain.model.Deal +import org.example.ticket.domain.enum.TicketStatus +import org.example.ticket.domain.model.Ticket + +class DealService (){ + + fun dealStart(ticket: Ticket, buyerName:String): Deal{ + val deal = Deal(ticket.sellerName, buyerName) + ticket.ticketStatus = TicketStatus.RESERVED + return deal + } + +} \ No newline at end of file diff --git a/src/main/kotlin/org/example/deal/domain/enum/DealStatus.kt b/src/main/kotlin/org/example/deal/domain/enum/DealStatus.kt new file mode 100644 index 0000000..3094b42 --- /dev/null +++ b/src/main/kotlin/org/example/deal/domain/enum/DealStatus.kt @@ -0,0 +1,7 @@ +package org.example.deal.domain.enum + +enum class DealStatus { + RESERVED, + COMPLETED, + CANCELLED, +} \ No newline at end of file diff --git a/src/main/kotlin/org/example/deal/domain/model/Deal.kt b/src/main/kotlin/org/example/deal/domain/model/Deal.kt new file mode 100644 index 0000000..eeb83c4 --- /dev/null +++ b/src/main/kotlin/org/example/deal/domain/model/Deal.kt @@ -0,0 +1,31 @@ +package org.example.deal.domain.model + +import org.example.deal.domain.enum.DealStatus +import java.time.LocalDateTime + +class Deal( + val sellerName: String, + val buyerName: String, + val reservedDateTime: LocalDateTime = LocalDateTime.now(), + var dealStatus: DealStatus = DealStatus.RESERVED +) { + companion object {} + + init { + require(sellerName != buyerName) { "본인이 등록이 올린 티켓은 구매할 수 없습니다." } + } + + fun dealComplete(){ + require(dealStatus == DealStatus.RESERVED) { "예약 상태인 거래만 완료할 수 있습니다." } + dealStatus = DealStatus.COMPLETED + + } + fun dealCancel(){ + require(dealStatus == DealStatus.RESERVED) { "예약 상태인 거래만 완료할 수 있습니다." } + dealStatus = DealStatus.COMPLETED + } + fun reservedTimeExpiredCheck():Boolean{ + return dealStatus == DealStatus.RESERVED && reservedDateTime.plusMinutes(10).isBefore(LocalDateTime.now()) + } + +} From ad069792566e9ff68062d76437fe952c94cbc4bd Mon Sep 17 00:00:00 2001 From: songhansol Date: Thu, 26 Feb 2026 18:25:57 +0900 Subject: [PATCH 10/22] =?UTF-8?q?=EC=96=B4=ED=94=8C=EB=A6=AC=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=ED=8C=A8=ED=82=A4=EC=A7=95=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=83=81=EC=9C=84=EB=A1=9C=20=EC=9D=B4=EB=8F=99=20?= =?UTF-8?q?=EB=94=9C=EC=97=90=20=EB=B0=94=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(=EB=AC=B4=EC=8A=A8=20=ED=8B=B0=EC=BC=93=EC=9D=84=20?= =?UTF-8?q?=EA=B1=B0=EB=9E=98=ED=95=98=EB=8A=94=EC=A7=80=20=EB=94=9C?= =?UTF-8?q?=EC=9D=B4=20=EC=95=8C=20=EC=88=98=EA=B0=80=20=EC=97=86=EC=9D=8C?= =?UTF-8?q?)=20=EA=B1=B0=EB=9E=98=EC=99=84=EB=A3=8C(dealComplete)=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=94=9C=EC=9D=B4=20=EC=99=84=EB=A3=8C?= =?UTF-8?q?=EB=90=98=EC=96=B4=20=ED=8C=90=EB=A7=A4=EC=A2=85=EB=A3=8C(SOLD)?= =?UTF-8?q?=EB=90=9C=20=ED=8B=B0=EC=BC=93=20=EC=9E=AC=ED=8C=90=EB=A7=A4=20?= =?UTF-8?q?=ED=95=A0=EC=88=98=EC=9E=87=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deal/application/service/DealService.kt | 20 ++++++++++++++++--- .../org/example/deal/domain/model/Deal.kt | 5 ++++- .../org/example/ticket/TicketApplication.kt | 11 ---------- .../service/TicketService/TicketService.kt | 18 ++++++++++++----- .../org/example/ticket/domain/model/Ticket.kt | 9 +++++++-- .../org/example/user/domain/model/User.kt | 18 +++++++++++++++-- .../example/user/repository/UserRepository.kt | 12 +++++++---- .../example/ticket/TicketApplicationTests.kt | 13 ------------ .../org/example/ticket/domain/TicketTest.kt | 4 ++-- 9 files changed, 67 insertions(+), 43 deletions(-) delete mode 100644 src/main/kotlin/org/example/ticket/TicketApplication.kt delete mode 100644 src/test/kotlin/org/example/ticket/TicketApplicationTests.kt diff --git a/src/main/kotlin/org/example/deal/application/service/DealService.kt b/src/main/kotlin/org/example/deal/application/service/DealService.kt index 7d2ca39..6166495 100644 --- a/src/main/kotlin/org/example/deal/application/service/DealService.kt +++ b/src/main/kotlin/org/example/deal/application/service/DealService.kt @@ -3,13 +3,27 @@ package org.example.deal.application.service import org.example.deal.domain.model.Deal import org.example.ticket.domain.enum.TicketStatus import org.example.ticket.domain.model.Ticket +import org.example.user.repository.UserRepository -class DealService (){ +class DealService ( + private val userRepository: UserRepository, +){ fun dealStart(ticket: Ticket, buyerName:String): Deal{ - val deal = Deal(ticket.sellerName, buyerName) - ticket.ticketStatus = TicketStatus.RESERVED + val sellingPrice = requireNotNull(ticket.sellingPrice) { "판매 가격이 설정되지 않은 티켓은 거래할 수 없습니다." } + val deal = Deal(ticket.barcode, ticket.sellerName, buyerName, sellingPrice); + ticket.ticketReserve(); return deal } + fun dealComplete(deal: Deal, ticket: Ticket){ + val seller = userRepository.findByName(deal.sellerName) + val buyer = userRepository.findByName(deal.buyerName) + requireNotNull(seller){"판매자 정보를 찾을 수 없습니다."} + requireNotNull(buyer){"구매자 정보를 찾을 수 없습니다."} + buyer.withdraw(deal.sellingPrice) + seller.deposit(deal.sellingPrice) + ticket.ticketSold() + } + } \ No newline at end of file diff --git a/src/main/kotlin/org/example/deal/domain/model/Deal.kt b/src/main/kotlin/org/example/deal/domain/model/Deal.kt index eeb83c4..dc3be89 100644 --- a/src/main/kotlin/org/example/deal/domain/model/Deal.kt +++ b/src/main/kotlin/org/example/deal/domain/model/Deal.kt @@ -1,18 +1,21 @@ package org.example.deal.domain.model import org.example.deal.domain.enum.DealStatus +import java.math.BigDecimal import java.time.LocalDateTime class Deal( + val barcode: String, val sellerName: String, val buyerName: String, + val sellingPrice: BigDecimal, val reservedDateTime: LocalDateTime = LocalDateTime.now(), var dealStatus: DealStatus = DealStatus.RESERVED ) { companion object {} init { - require(sellerName != buyerName) { "본인이 등록이 올린 티켓은 구매할 수 없습니다." } + require(sellerName != buyerName) { "본인이 등록한 티켓은 구매할 수 없습니다." } } fun dealComplete(){ diff --git a/src/main/kotlin/org/example/ticket/TicketApplication.kt b/src/main/kotlin/org/example/ticket/TicketApplication.kt deleted file mode 100644 index 36c0af1..0000000 --- a/src/main/kotlin/org/example/ticket/TicketApplication.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.example.ticket - -import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.boot.runApplication - -@SpringBootApplication -class TicketApplication - -fun main(args: Array) { - runApplication(*args) -} diff --git a/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt b/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt index affd8ab..999a72c 100644 --- a/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt +++ b/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt @@ -1,6 +1,7 @@ package org.example.ticket.application.service.TicketService import org.example.ticket.application.dto.TicketCreationDto +import org.example.ticket.domain.enum.TicketStatus import org.example.ticket.domain.model.Ticket import org.example.ticket.infra.repository.TicketJpaRepository import org.example.ticket.infrastructure.api.TicketApiClient @@ -9,11 +10,11 @@ import org.springframework.stereotype.Service import java.math.BigDecimal @Service -class TicketService ( +class TicketService( private val ticketApiClientResolver: TicketApiClientResolver, private val ticketRepository: TicketJpaRepository ) { - fun createTicket(ticketCreationDto: TicketCreationDto){ + fun createTicket(ticketCreationDto: TicketCreationDto) { val apiClient = ticketApiClientResolver.resolve(ticketCreationDto.barcode) val ticketResponseDto = apiClient.getTicket(ticketCreationDto.barcode) val ticket = ticketResponseDto.toTicket( @@ -22,14 +23,21 @@ class TicketService ( apiClient.type(), ) ticketRepository.save(ticket) - }; fun applySellerOfferPrice(offerSellerName: String, barcode: String, offerPrice: BigDecimal) { val ticket = ticketRepository.findByBarcode(barcode) - requireNotNull(ticket){"해당 티켓 정보가 존재하지 않습니다."} - + requireNotNull(ticket) { "해당 티켓 정보가 존재하지 않습니다." } ticket.applySellerOfferPrice(offerSellerName, offerPrice); ticketRepository.save(ticket) } + + fun reSaleTicket(offerSellerName: String, barcode: String, offerPrice: BigDecimal) { + val ticket = ticketRepository.findByBarcode(barcode) + requireNotNull(ticket) { "해당 티켓 정보가 존재하지 않습니다." } + ticket.applySellerOfferPrice(offerSellerName, offerPrice) + ticket.ticketStatus = TicketStatus.ON_SALE + ticketRepository.save(ticket) + + } } \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt index 80092e5..84e4d41 100644 --- a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt +++ b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt @@ -19,7 +19,7 @@ class Ticket( val originalPrice: BigDecimal, // BigDecimal => 10진수로 저장(소수점 계산을 위해) val sellerName: String, var sellingPrice: BigDecimal? = null, - var ticketStatus: TicketStatus = TicketStatus.ON_SALE, + private var ticketStatus: TicketStatus = TicketStatus.ON_SALE, val ticketType: TicketType ) { companion object { @@ -68,9 +68,14 @@ class Ticket( sellingPrice = offerPrice } + fun reSale(offerSellerName: String, offerPrice: BigDecimal) { + require(ticketStatus == TicketStatus.SOLD) { "구매한 티켓만 판매할 수 있습니다." } + ticketStatus = TicketStatus.ON_SALE + applySellerOfferPrice(offerSellerName, offerPrice); + } fun ticketOnSale() { - require(ticketStatus == TicketStatus.RESERVED) { "예약취소된 티켓만 재판매 할 수 있습니다." } + require(ticketStatus == TicketStatus.SOLD) { "구매한 티켓만 재판매 할 수 있습니다." } ticketStatus = TicketStatus.ON_SALE } diff --git a/src/main/kotlin/org/example/user/domain/model/User.kt b/src/main/kotlin/org/example/user/domain/model/User.kt index 2ea6e3c..51b92b8 100644 --- a/src/main/kotlin/org/example/user/domain/model/User.kt +++ b/src/main/kotlin/org/example/user/domain/model/User.kt @@ -2,11 +2,25 @@ package org.example.user.domain.model import org.example.user.domain.enum.UserRole +import java.math.BigDecimal + class User ( val id: Long? = null, - val name : String, - val role : UserRole + val name: String, + val role: UserRole, + var money: BigDecimal = BigDecimal.ZERO, ){ companion object{} init {} + fun deposit(amount: BigDecimal) { + require(amount > BigDecimal.ZERO) { "입금액은 0보다 커야 합니다." } + money = money.add(amount) + } + + fun withdraw(amount: BigDecimal) { + require(amount > BigDecimal.ZERO) { "출금액은 0보다 커야 합니다." } + require(money >= amount) { "잔액이 부족합니다." } + money = money.subtract(amount) + } + } \ No newline at end of file diff --git a/src/main/kotlin/org/example/user/repository/UserRepository.kt b/src/main/kotlin/org/example/user/repository/UserRepository.kt index b11be7b..1042b54 100644 --- a/src/main/kotlin/org/example/user/repository/UserRepository.kt +++ b/src/main/kotlin/org/example/user/repository/UserRepository.kt @@ -2,14 +2,18 @@ package org.example.user.repository import org.example.user.domain.enum.UserRole import org.example.user.domain.model.User +import java.math.BigDecimal object UserRepository { private val users = listOf( - User(id = 1, name = "판매자1", role = UserRole.SELLER), - User(id = 2, name = "판매자2", role = UserRole.SELLER), - User(id = 3, name = "구매자1", role = UserRole.BUYER), - User(id = 4, name = "구매자2", role = UserRole.BUYER), + User(id = 1, name = "판매자1", role = UserRole.SELLER, money = BigDecimal.ZERO), + User(id = 2, name = "판매자2", role = UserRole.SELLER, money = BigDecimal.ZERO), + User(id = 3, name = "구매자1", role = UserRole.BUYER, money = BigDecimal(10000)), + User(id = 4, name = "구매자2", role = UserRole.BUYER, money = BigDecimal(50000)), ) fun findByName(name: String): User? = users.find { it.name == name } + + + } \ No newline at end of file diff --git a/src/test/kotlin/org/example/ticket/TicketApplicationTests.kt b/src/test/kotlin/org/example/ticket/TicketApplicationTests.kt deleted file mode 100644 index aca8487..0000000 --- a/src/test/kotlin/org/example/ticket/TicketApplicationTests.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.example.ticket - -import org.junit.jupiter.api.Test -import org.springframework.boot.test.context.SpringBootTest - -@SpringBootTest -class TicketApplicationTests { - - @Test - fun contextLoads() { - } - -} diff --git a/src/test/kotlin/org/example/ticket/domain/TicketTest.kt b/src/test/kotlin/org/example/ticket/domain/TicketTest.kt index 737806d..947d36c 100644 --- a/src/test/kotlin/org/example/ticket/domain/TicketTest.kt +++ b/src/test/kotlin/org/example/ticket/domain/TicketTest.kt @@ -34,7 +34,7 @@ class TicketTest { Ticket( barcode = "ABCDEFGH", sellerName = "Seller A", - expirationDateTime = Ticket.getTicketDeadLine().minusSeconds(1), + expirationDateTime = LocalDateTime.now().plusHours(1).minusSeconds(1), originalPrice = BigDecimal.TEN, ticketType = TicketType.MELON ) @@ -48,7 +48,7 @@ class TicketTest { id = 1L, barcode = "ABCDEFGH", sellerName = "Seller A", - expirationDateTime = Ticket.getTicketDeadLine().plusSeconds(1), + expirationDateTime = LocalDateTime.now().plusHours(1).minusSeconds(1), originalPrice = ticketPrice, ticketType = TicketType.MELON ) From 0089c123049d1ee9932da9ff56b78360cca431db Mon Sep 17 00:00:00 2001 From: songhansol Date: Thu, 26 Feb 2026 23:08:07 +0900 Subject: [PATCH 11/22] =?UTF-8?q?deal=20=EB=A0=88=ED=8D=BC=EC=A7=80?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EC=A0=80=EC=9E=A5/=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80,=20=EA=B0=81=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=84=9C=EB=B9=84=EC=8A=A4=20DTO?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deal/application/dto/DealEndDto.kt | 5 +++ .../deal/application/dto/DealStartDto.kt | 7 +++ .../deal/application/service/DealService.kt | 44 ++++++++++++++----- .../org/example/deal/domain/model/Deal.kt | 11 ++++- .../deal/repository/DealJpaRepository.kt | 9 ++++ .../dto/TicketSellingPriceOfferDto.kt | 10 +++++ .../service/TicketService/TicketService.kt | 26 +++++++---- .../org/example/ticket/domain/model/Ticket.kt | 5 +-- ...UserRepository.kt => UserJpaRepository.kt} | 2 +- 9 files changed, 92 insertions(+), 27 deletions(-) create mode 100644 src/main/kotlin/org/example/deal/application/dto/DealEndDto.kt create mode 100644 src/main/kotlin/org/example/deal/application/dto/DealStartDto.kt create mode 100644 src/main/kotlin/org/example/deal/repository/DealJpaRepository.kt create mode 100644 src/main/kotlin/org/example/ticket/application/dto/TicketSellingPriceOfferDto.kt rename src/main/kotlin/org/example/user/repository/{UserRepository.kt => UserJpaRepository.kt} (95%) diff --git a/src/main/kotlin/org/example/deal/application/dto/DealEndDto.kt b/src/main/kotlin/org/example/deal/application/dto/DealEndDto.kt new file mode 100644 index 0000000..1c6f822 --- /dev/null +++ b/src/main/kotlin/org/example/deal/application/dto/DealEndDto.kt @@ -0,0 +1,5 @@ +package org.example.deal.application.dto + +data class DealEndDto( + val barcode: String +) diff --git a/src/main/kotlin/org/example/deal/application/dto/DealStartDto.kt b/src/main/kotlin/org/example/deal/application/dto/DealStartDto.kt new file mode 100644 index 0000000..77dae54 --- /dev/null +++ b/src/main/kotlin/org/example/deal/application/dto/DealStartDto.kt @@ -0,0 +1,7 @@ +package org.example.deal.application.dto + +data class DealStartDto( + val barcode: String, + val buyerName: String, +) + diff --git a/src/main/kotlin/org/example/deal/application/service/DealService.kt b/src/main/kotlin/org/example/deal/application/service/DealService.kt index 6166495..5d6dce9 100644 --- a/src/main/kotlin/org/example/deal/application/service/DealService.kt +++ b/src/main/kotlin/org/example/deal/application/service/DealService.kt @@ -1,28 +1,48 @@ package org.example.deal.application.service +import org.example.deal.application.dto.DealEndDto +import org.example.deal.application.dto.DealStartDto import org.example.deal.domain.model.Deal -import org.example.ticket.domain.enum.TicketStatus +import org.example.deal.repository.DealJpaRepository import org.example.ticket.domain.model.Ticket -import org.example.user.repository.UserRepository +import org.example.ticket.infra.repository.TicketJpaRepository +import org.example.user.repository.UserJpaRepository class DealService ( - private val userRepository: UserRepository, + private val userRepository: UserJpaRepository, + private val dealRepository: DealJpaRepository, + private val ticketRepository: TicketJpaRepository ){ - fun dealStart(ticket: Ticket, buyerName:String): Deal{ - val sellingPrice = requireNotNull(ticket.sellingPrice) { "판매 가격이 설정되지 않은 티켓은 거래할 수 없습니다." } - val deal = Deal(ticket.barcode, ticket.sellerName, buyerName, sellingPrice); + fun dealStart(dealStartDto: DealStartDto){ + val ticket = requireNotNull(ticketRepository.findByBarcode(dealStartDto.barcode)){"존재하지 않는 티켓입니다."} + val sellingPrice = requireNotNull(ticket.sellingPrice){"판매가가 설정되지 않았습니다."} + + val deal = Deal( + barcode = ticket.barcode, + sellerName = ticket.sellerName, + buyerName = dealStartDto.buyerName, + sellingPrice = sellingPrice + ) + ticket.ticketReserve(); - return deal + dealRepository.save(deal) } - fun dealComplete(deal: Deal, ticket: Ticket){ - val seller = userRepository.findByName(deal.sellerName) - val buyer = userRepository.findByName(deal.buyerName) - requireNotNull(seller){"판매자 정보를 찾을 수 없습니다."} - requireNotNull(buyer){"구매자 정보를 찾을 수 없습니다."} + fun dealEnd(dealEndDto: DealEndDto){ + val deal = requireNotNull(dealRepository.findByBarcode(dealEndDto.barcode)){"존재하지 않는 거래입니다."} + val ticket = requireNotNull(ticketRepository.findByBarcode(dealEndDto.barcode)){"존재하지 않는 티켓입니다."} + val dealExpiredCheck = deal.reservedTimeExpiredCheck(); + if(dealExpiredCheck){ + deal.dealCancel() + ticket.ticketOnSale() + throw IllegalArgumentException("10분 이내 입금이 되지않아 결제가 취소되었습니다.") + } + val seller = requireNotNull(userRepository.findByName(deal.sellerName)){"판매자 정보를 찾을 수 없습니다."} + val buyer = requireNotNull(userRepository.findByName(deal.buyerName)){"구매자 정보를 찾을 수 없습니다."} buyer.withdraw(deal.sellingPrice) seller.deposit(deal.sellingPrice) + deal.dealComplete() ticket.ticketSold() } diff --git a/src/main/kotlin/org/example/deal/domain/model/Deal.kt b/src/main/kotlin/org/example/deal/domain/model/Deal.kt index dc3be89..a7c2439 100644 --- a/src/main/kotlin/org/example/deal/domain/model/Deal.kt +++ b/src/main/kotlin/org/example/deal/domain/model/Deal.kt @@ -1,10 +1,17 @@ package org.example.deal.domain.model +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id import org.example.deal.domain.enum.DealStatus import java.math.BigDecimal import java.time.LocalDateTime +@Entity class Deal( + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, val barcode: String, val sellerName: String, val buyerName: String, @@ -24,8 +31,8 @@ class Deal( } fun dealCancel(){ - require(dealStatus == DealStatus.RESERVED) { "예약 상태인 거래만 완료할 수 있습니다." } - dealStatus = DealStatus.COMPLETED + require(dealStatus == DealStatus.RESERVED) { "예약 상태인 거래만 취소 할 수 있습니다." } + dealStatus = DealStatus.CANCELLED } fun reservedTimeExpiredCheck():Boolean{ return dealStatus == DealStatus.RESERVED && reservedDateTime.plusMinutes(10).isBefore(LocalDateTime.now()) diff --git a/src/main/kotlin/org/example/deal/repository/DealJpaRepository.kt b/src/main/kotlin/org/example/deal/repository/DealJpaRepository.kt new file mode 100644 index 0000000..7143dc4 --- /dev/null +++ b/src/main/kotlin/org/example/deal/repository/DealJpaRepository.kt @@ -0,0 +1,9 @@ +package org.example.deal.repository + +import org.example.deal.domain.model.Deal +import org.springframework.data.jpa.repository.JpaRepository + +interface DealJpaRepository: JpaRepository { + fun findByBarcode(barcode: String): Deal? + fun findBySellerName(sellerName: String): Deal? +} diff --git a/src/main/kotlin/org/example/ticket/application/dto/TicketSellingPriceOfferDto.kt b/src/main/kotlin/org/example/ticket/application/dto/TicketSellingPriceOfferDto.kt new file mode 100644 index 0000000..2c83267 --- /dev/null +++ b/src/main/kotlin/org/example/ticket/application/dto/TicketSellingPriceOfferDto.kt @@ -0,0 +1,10 @@ +package org.example.ticket.application.dto + +import java.math.BigDecimal + +data class TicketSellingPriceOfferDto( + val sellerName: String, + val barcode: String, + val sellingPrice: BigDecimal, + +) diff --git a/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt b/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt index 999a72c..2a9c5e0 100644 --- a/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt +++ b/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt @@ -1,6 +1,7 @@ package org.example.ticket.application.service.TicketService import org.example.ticket.application.dto.TicketCreationDto +import org.example.ticket.application.dto.TicketSellingPriceOfferDto import org.example.ticket.domain.enum.TicketStatus import org.example.ticket.domain.model.Ticket import org.example.ticket.infra.repository.TicketJpaRepository @@ -25,18 +26,25 @@ class TicketService( ticketRepository.save(ticket) }; - fun applySellerOfferPrice(offerSellerName: String, barcode: String, offerPrice: BigDecimal) { - val ticket = ticketRepository.findByBarcode(barcode) - requireNotNull(ticket) { "해당 티켓 정보가 존재하지 않습니다." } - ticket.applySellerOfferPrice(offerSellerName, offerPrice); + fun applySellerOfferPrice(ticketSellingPriceOfferDto: TicketSellingPriceOfferDto) { + val ticket = requireNotNull(ticketRepository.findByBarcode(ticketSellingPriceOfferDto.barcode)){ + "해당 티켓 정보가 존재하지 않습니다." + } + ticket.applySellerOfferPrice( + ticketSellingPriceOfferDto.sellerName, + ticketSellingPriceOfferDto.sellingPrice + ) ticketRepository.save(ticket) } - fun reSaleTicket(offerSellerName: String, barcode: String, offerPrice: BigDecimal) { - val ticket = ticketRepository.findByBarcode(barcode) - requireNotNull(ticket) { "해당 티켓 정보가 존재하지 않습니다." } - ticket.applySellerOfferPrice(offerSellerName, offerPrice) - ticket.ticketStatus = TicketStatus.ON_SALE + fun reSaleTicket(ticketSellingPriceOfferDto: TicketSellingPriceOfferDto) { + val ticket = requireNotNull(ticketRepository.findByBarcode(ticketSellingPriceOfferDto.barcode)){ + "해당 티켓 정보가 존재하지 않습니다." + } + ticket.reSale( + ticketSellingPriceOfferDto.sellerName, + ticketSellingPriceOfferDto.sellingPrice + ) ticketRepository.save(ticket) } diff --git a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt index 84e4d41..5f6ed91 100644 --- a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt +++ b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt @@ -6,7 +6,6 @@ import jakarta.persistence.GenerationType import jakarta.persistence.Id import org.example.ticket.domain.enum.TicketStatus import org.example.ticket.domain.enum.TicketType -import org.springframework.cglib.core.Local import java.math.BigDecimal import java.time.LocalDateTime @@ -75,12 +74,12 @@ class Ticket( } fun ticketOnSale() { - require(ticketStatus == TicketStatus.SOLD) { "구매한 티켓만 재판매 할 수 있습니다." } + require(ticketStatus == TicketStatus.RESERVED) { "예약중인 티켓만 판매중으로 되돌릴 수 있습니다." } ticketStatus = TicketStatus.ON_SALE } fun ticketReserve() { - require(ticketStatus == TicketStatus.ON_SALE) { "예약중인 티켓만 판매완료할 수 있습니다." } + require(ticketStatus == TicketStatus.ON_SALE) { "판매중인 티켓만 예약할 수 있습니다" } ticketStatus = TicketStatus.RESERVED } diff --git a/src/main/kotlin/org/example/user/repository/UserRepository.kt b/src/main/kotlin/org/example/user/repository/UserJpaRepository.kt similarity index 95% rename from src/main/kotlin/org/example/user/repository/UserRepository.kt rename to src/main/kotlin/org/example/user/repository/UserJpaRepository.kt index 1042b54..a3b75d9 100644 --- a/src/main/kotlin/org/example/user/repository/UserRepository.kt +++ b/src/main/kotlin/org/example/user/repository/UserJpaRepository.kt @@ -4,7 +4,7 @@ import org.example.user.domain.enum.UserRole import org.example.user.domain.model.User import java.math.BigDecimal -object UserRepository { +object UserJpaRepository { private val users = listOf( User(id = 1, name = "판매자1", role = UserRole.SELLER, money = BigDecimal.ZERO), User(id = 2, name = "판매자2", role = UserRole.SELLER, money = BigDecimal.ZERO), From 80fe488db3131484e5acd4f65fbab30ddefb191e Mon Sep 17 00:00:00 2001 From: songhansol Date: Fri, 27 Feb 2026 16:02:48 +0900 Subject: [PATCH 12/22] =?UTF-8?q?deal=20paymentGateway=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/example/SecondChanceApplication.kt | 11 ++++++++++ .../deal/application/dto/DealEndDto.kt | 5 ++++- .../deal/application/service/DealService.kt | 11 +++++----- .../example/deal/domain/enum/PaymentType.kt | 6 ++++++ .../infrastructure/KakaoPaymentGateway.kt | 20 +++++++++++++++++++ .../deal/infrastructure/PaymentGateway.kt | 9 +++++++++ .../infrastructure/PaymentGatewayResolver.kt | 12 +++++++++++ .../deal/infrastructure/TossPaymentGateway.kt | 20 +++++++++++++++++++ .../infrastructure/api/MolTicketApiClient.kt | 2 +- .../example/SecondChanceApplicationTests.kt | 13 ++++++++++++ 10 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 src/main/kotlin/org/example/SecondChanceApplication.kt create mode 100644 src/main/kotlin/org/example/deal/domain/enum/PaymentType.kt create mode 100644 src/main/kotlin/org/example/deal/infrastructure/KakaoPaymentGateway.kt create mode 100644 src/main/kotlin/org/example/deal/infrastructure/PaymentGateway.kt create mode 100644 src/main/kotlin/org/example/deal/infrastructure/PaymentGatewayResolver.kt create mode 100644 src/main/kotlin/org/example/deal/infrastructure/TossPaymentGateway.kt create mode 100644 src/test/kotlin/org/example/SecondChanceApplicationTests.kt diff --git a/src/main/kotlin/org/example/SecondChanceApplication.kt b/src/main/kotlin/org/example/SecondChanceApplication.kt new file mode 100644 index 0000000..66be87e --- /dev/null +++ b/src/main/kotlin/org/example/SecondChanceApplication.kt @@ -0,0 +1,11 @@ +package org.example + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class SecondChanceApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/src/main/kotlin/org/example/deal/application/dto/DealEndDto.kt b/src/main/kotlin/org/example/deal/application/dto/DealEndDto.kt index 1c6f822..3d1d194 100644 --- a/src/main/kotlin/org/example/deal/application/dto/DealEndDto.kt +++ b/src/main/kotlin/org/example/deal/application/dto/DealEndDto.kt @@ -1,5 +1,8 @@ package org.example.deal.application.dto +import org.example.deal.domain.enum.PaymentType + data class DealEndDto( - val barcode: String + val barcode: String, + val payementType: PaymentType ) diff --git a/src/main/kotlin/org/example/deal/application/service/DealService.kt b/src/main/kotlin/org/example/deal/application/service/DealService.kt index 5d6dce9..86c4e51 100644 --- a/src/main/kotlin/org/example/deal/application/service/DealService.kt +++ b/src/main/kotlin/org/example/deal/application/service/DealService.kt @@ -3,6 +3,8 @@ package org.example.deal.application.service import org.example.deal.application.dto.DealEndDto import org.example.deal.application.dto.DealStartDto import org.example.deal.domain.model.Deal +import org.example.deal.infrastructure.KakaoPaymentGateway +import org.example.deal.infrastructure.PaymentGatewayResolver import org.example.deal.repository.DealJpaRepository import org.example.ticket.domain.model.Ticket import org.example.ticket.infra.repository.TicketJpaRepository @@ -11,7 +13,8 @@ import org.example.user.repository.UserJpaRepository class DealService ( private val userRepository: UserJpaRepository, private val dealRepository: DealJpaRepository, - private val ticketRepository: TicketJpaRepository + private val ticketRepository: TicketJpaRepository, + private val paymentGatewayResolver: PaymentGatewayResolver ){ fun dealStart(dealStartDto: DealStartDto){ @@ -38,10 +41,8 @@ class DealService ( ticket.ticketOnSale() throw IllegalArgumentException("10분 이내 입금이 되지않아 결제가 취소되었습니다.") } - val seller = requireNotNull(userRepository.findByName(deal.sellerName)){"판매자 정보를 찾을 수 없습니다."} - val buyer = requireNotNull(userRepository.findByName(deal.buyerName)){"구매자 정보를 찾을 수 없습니다."} - buyer.withdraw(deal.sellingPrice) - seller.deposit(deal.sellingPrice) + val paymentGateway = paymentGatewayResolver.resolve(dealEndDto.payementType) + paymentGateway.pay(deal.buyerName, deal.sellerName, deal.sellingPrice) deal.dealComplete() ticket.ticketSold() } diff --git a/src/main/kotlin/org/example/deal/domain/enum/PaymentType.kt b/src/main/kotlin/org/example/deal/domain/enum/PaymentType.kt new file mode 100644 index 0000000..b9b7c78 --- /dev/null +++ b/src/main/kotlin/org/example/deal/domain/enum/PaymentType.kt @@ -0,0 +1,6 @@ +package org.example.deal.domain.enum + +enum class PaymentType { + KAKAO, + TOSS +} \ No newline at end of file diff --git a/src/main/kotlin/org/example/deal/infrastructure/KakaoPaymentGateway.kt b/src/main/kotlin/org/example/deal/infrastructure/KakaoPaymentGateway.kt new file mode 100644 index 0000000..6c2c42c --- /dev/null +++ b/src/main/kotlin/org/example/deal/infrastructure/KakaoPaymentGateway.kt @@ -0,0 +1,20 @@ +package org.example.deal.infrastructure + +import org.example.deal.domain.enum.PaymentType +import org.springframework.stereotype.Component +import org.example.user.repository.UserJpaRepository +import java.math.BigDecimal + + +@Component +class KakaoPaymentGateway: PaymentGateway { + override fun pay(buyerName: String, sellerName: String, price: BigDecimal) { + val buyer = requireNotNull(UserJpaRepository.findByName(buyerName)) { "구매자 정보를 찾을 수 없습니다." } + val seller = requireNotNull(UserJpaRepository.findByName(sellerName)) { "판매자 정보를 찾을 수 없습니다." } + buyer.withdraw(price) + seller.deposit(price) + } + override fun type(): PaymentType { + return PaymentType.KAKAO + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/example/deal/infrastructure/PaymentGateway.kt b/src/main/kotlin/org/example/deal/infrastructure/PaymentGateway.kt new file mode 100644 index 0000000..b4936e3 --- /dev/null +++ b/src/main/kotlin/org/example/deal/infrastructure/PaymentGateway.kt @@ -0,0 +1,9 @@ +package org.example.deal.infrastructure + +import org.example.deal.domain.enum.PaymentType +import java.math.BigDecimal + +interface PaymentGateway{ + fun pay(buyerName:String, sellerName:String, price: BigDecimal) + fun type(): PaymentType +} \ No newline at end of file diff --git a/src/main/kotlin/org/example/deal/infrastructure/PaymentGatewayResolver.kt b/src/main/kotlin/org/example/deal/infrastructure/PaymentGatewayResolver.kt new file mode 100644 index 0000000..5bd6e72 --- /dev/null +++ b/src/main/kotlin/org/example/deal/infrastructure/PaymentGatewayResolver.kt @@ -0,0 +1,12 @@ +package org.example.deal.infrastructure + +import org.example.deal.domain.enum.PaymentType + +class PaymentGatewayResolver( + private val gateways: List +) { + fun resolve(offerPaymentType: PaymentType): PaymentGateway { + val paymentType = gateways.find { it.type() == offerPaymentType } + return paymentType ?: throw IllegalArgumentException("지원하지 않는 결제 수단입니다.") + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/example/deal/infrastructure/TossPaymentGateway.kt b/src/main/kotlin/org/example/deal/infrastructure/TossPaymentGateway.kt new file mode 100644 index 0000000..8236ce4 --- /dev/null +++ b/src/main/kotlin/org/example/deal/infrastructure/TossPaymentGateway.kt @@ -0,0 +1,20 @@ +package org.example.deal.infrastructure + +import org.example.deal.domain.enum.PaymentType +import org.example.user.repository.UserJpaRepository +import org.springframework.stereotype.Component +import java.math.BigDecimal + +@Component +class TossPaymentGateway: PaymentGateway { + override fun pay(buyerName: String, sellerName: String, price: BigDecimal) { + val buyer = requireNotNull(UserJpaRepository.findByName(buyerName)) { "구매자 정보를 찾을 수 없습니다." } + val seller = requireNotNull(UserJpaRepository.findByName(sellerName)) { "판매자 정보를 찾을 수 없습니다." } + buyer.withdraw(price) + seller.deposit(price) + } + + override fun type(): PaymentType { + return PaymentType.TOSS + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/infrastructure/api/MolTicketApiClient.kt b/src/main/kotlin/org/example/ticket/infrastructure/api/MolTicketApiClient.kt index 475d850..c4e9e56 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/api/MolTicketApiClient.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/api/MolTicketApiClient.kt @@ -15,6 +15,6 @@ class MolTicketApiClient: TicketApiClient { ) } override fun type(): TicketType { - return TicketType.MELON + return TicketType.MOL } } \ No newline at end of file diff --git a/src/test/kotlin/org/example/SecondChanceApplicationTests.kt b/src/test/kotlin/org/example/SecondChanceApplicationTests.kt new file mode 100644 index 0000000..62faf02 --- /dev/null +++ b/src/test/kotlin/org/example/SecondChanceApplicationTests.kt @@ -0,0 +1,13 @@ +package org.example + +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class SecondChanceApplicationTests { + + @Test + fun contextLoads() { + } + +} From 6569cfa81a538a14339eb841ad27275ff25c2ab1 Mon Sep 17 00:00:00 2001 From: songhansol Date: Fri, 27 Feb 2026 17:24:06 +0900 Subject: [PATCH 13/22] =?UTF-8?q?=EB=94=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/example/deal/domain/model/Deal.kt | 6 +- .../org/example/deal/domain/DealTest.kt | 65 +++++++++++++++++++ .../org/example/ticket/domain/TicketTest.kt | 6 +- 3 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 src/test/kotlin/org/example/deal/domain/DealTest.kt diff --git a/src/main/kotlin/org/example/deal/domain/model/Deal.kt b/src/main/kotlin/org/example/deal/domain/model/Deal.kt index a7c2439..2a0080b 100644 --- a/src/main/kotlin/org/example/deal/domain/model/Deal.kt +++ b/src/main/kotlin/org/example/deal/domain/model/Deal.kt @@ -17,7 +17,7 @@ class Deal( val buyerName: String, val sellingPrice: BigDecimal, val reservedDateTime: LocalDateTime = LocalDateTime.now(), - var dealStatus: DealStatus = DealStatus.RESERVED + private var dealStatus: DealStatus = DealStatus.RESERVED ) { companion object {} @@ -37,5 +37,7 @@ class Deal( fun reservedTimeExpiredCheck():Boolean{ return dealStatus == DealStatus.RESERVED && reservedDateTime.plusMinutes(10).isBefore(LocalDateTime.now()) } - + fun getDealStatus(): DealStatus { + return dealStatus + } } diff --git a/src/test/kotlin/org/example/deal/domain/DealTest.kt b/src/test/kotlin/org/example/deal/domain/DealTest.kt new file mode 100644 index 0000000..eca3d8f --- /dev/null +++ b/src/test/kotlin/org/example/deal/domain/DealTest.kt @@ -0,0 +1,65 @@ +package org.example.deal.domain + +import org.example.deal.domain.model.Deal +import org.example.deal.domain.enum.DealStatus +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.assertj.core.api.Assertions.assertThat +import org.example.ticket.domain.enum.TicketType +import org.example.ticket.domain.model.Ticket +import java.math.BigDecimal +import java.time.LocalDateTime + +class DealTest { + + @Test + fun `판매자는 자신의 티켓을 구매할 수 없다`() { + assertThrows { + Deal( + barcode = "ABCDEFGH", + sellerName = "판매자1", + buyerName = "판매자1", + sellingPrice = BigDecimal(10000) + ) + } + } + + @Test + fun `거래 생성 시 상태는 RESERVED이다`() { + val deal = Deal( + barcode = "ABCDEFGH", + sellerName = "판매자1", + buyerName = "구매자1", + sellingPrice = BigDecimal(10000) + ) + assertThat(deal.getDealStatus()).isEqualTo(DealStatus.RESERVED) + } + + + @Test + fun `예약 상태인 거래만 완료할 수 있다`() { + val deal = Deal( + barcode = "ABCDEFGH", + sellerName = "판매자1", + buyerName = "구매자1", + sellingPrice = BigDecimal(10000), + dealStatus = DealStatus.COMPLETED + ) + assertThrows { + deal.dealComplete() + } + } + + @Test + fun `10분이 지나면 예약이 만료된다`() { + val deal = Deal( + barcode = "ABCDEFGH", + sellerName = "판매자1", + buyerName = "구매자1", + sellingPrice = BigDecimal(10000), + reservedDateTime = LocalDateTime.now().minusMinutes(11) // 11분 전 + ) + assertThat(deal.reservedTimeExpiredCheck()).isTrue() + } +} + diff --git a/src/test/kotlin/org/example/ticket/domain/TicketTest.kt b/src/test/kotlin/org/example/ticket/domain/TicketTest.kt index 947d36c..2c3ba7e 100644 --- a/src/test/kotlin/org/example/ticket/domain/TicketTest.kt +++ b/src/test/kotlin/org/example/ticket/domain/TicketTest.kt @@ -43,18 +43,18 @@ class TicketTest { @Test fun `공연 당일에는 가격을 정가의 50% 이하로만 설정 할 수 있다`() { - val ticketPrice = BigDecimal.TEN + val ticketPrice = BigDecimal(10000) val ticket = Ticket( id = 1L, barcode = "ABCDEFGH", sellerName = "Seller A", - expirationDateTime = LocalDateTime.now().plusHours(1).minusSeconds(1), + expirationDateTime = LocalDateTime.now().plusHours(2), originalPrice = ticketPrice, ticketType = TicketType.MELON ) assertThrows { - ticket.applySellerOfferPrice("seller_A", BigDecimal.valueOf(100.0)) + ticket.applySellerOfferPrice("Seller A", BigDecimal(6000)) } } From a59c62440685891377165d53145db11da0d17957 Mon Sep 17 00:00:00 2001 From: songhansol Date: Fri, 27 Feb 2026 18:06:14 +0900 Subject: [PATCH 14/22] =?UTF-8?q?=ED=8B=B0=EC=BC=93=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/dto/TicketResponseDto.kt | 14 +++++ .../dto/TicketSellingPriceOfferDto.kt | 1 - .../service/TicketService/TicketService.kt | 13 ++-- .../org/example/ticket/domain/model/Ticket.kt | 4 +- .../controller/TicketController.kt | 59 +++++++++++++++++++ 5 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 src/main/kotlin/org/example/ticket/application/dto/TicketResponseDto.kt create mode 100644 src/main/kotlin/org/example/ticket/presentation/controller/TicketController.kt diff --git a/src/main/kotlin/org/example/ticket/application/dto/TicketResponseDto.kt b/src/main/kotlin/org/example/ticket/application/dto/TicketResponseDto.kt new file mode 100644 index 0000000..c6b4aa3 --- /dev/null +++ b/src/main/kotlin/org/example/ticket/application/dto/TicketResponseDto.kt @@ -0,0 +1,14 @@ +package org.example.ticket.application.dto + +import org.example.ticket.domain.enum.TicketStatus +import java.math.BigDecimal +import java.time.LocalDateTime + +data class TicketResponseDto( + val barcode: String, + val seller: String, + val originalPrice: BigDecimal, + val sellingPrice: BigDecimal?, + val expirationDate: LocalDateTime, + val ticketStatus: TicketStatus +) diff --git a/src/main/kotlin/org/example/ticket/application/dto/TicketSellingPriceOfferDto.kt b/src/main/kotlin/org/example/ticket/application/dto/TicketSellingPriceOfferDto.kt index 2c83267..b61e198 100644 --- a/src/main/kotlin/org/example/ticket/application/dto/TicketSellingPriceOfferDto.kt +++ b/src/main/kotlin/org/example/ticket/application/dto/TicketSellingPriceOfferDto.kt @@ -6,5 +6,4 @@ data class TicketSellingPriceOfferDto( val sellerName: String, val barcode: String, val sellingPrice: BigDecimal, - ) diff --git a/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt b/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt index 2a9c5e0..6e7e43a 100644 --- a/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt +++ b/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt @@ -15,7 +15,7 @@ class TicketService( private val ticketApiClientResolver: TicketApiClientResolver, private val ticketRepository: TicketJpaRepository ) { - fun createTicket(ticketCreationDto: TicketCreationDto) { + fun createTicket(ticketCreationDto: TicketCreationDto): Ticket { val apiClient = ticketApiClientResolver.resolve(ticketCreationDto.barcode) val ticketResponseDto = apiClient.getTicket(ticketCreationDto.barcode) val ticket = ticketResponseDto.toTicket( @@ -23,10 +23,10 @@ class TicketService( ticketCreationDto.sellerName, apiClient.type(), ) - ticketRepository.save(ticket) + return ticketRepository.save(ticket) }; - fun applySellerOfferPrice(ticketSellingPriceOfferDto: TicketSellingPriceOfferDto) { + fun applySellerOfferPrice(ticketSellingPriceOfferDto: TicketSellingPriceOfferDto): Ticket { val ticket = requireNotNull(ticketRepository.findByBarcode(ticketSellingPriceOfferDto.barcode)){ "해당 티켓 정보가 존재하지 않습니다." } @@ -34,10 +34,10 @@ class TicketService( ticketSellingPriceOfferDto.sellerName, ticketSellingPriceOfferDto.sellingPrice ) - ticketRepository.save(ticket) + return ticketRepository.save(ticket) } - fun reSaleTicket(ticketSellingPriceOfferDto: TicketSellingPriceOfferDto) { + fun reSaleTicket(ticketSellingPriceOfferDto: TicketSellingPriceOfferDto): Ticket { val ticket = requireNotNull(ticketRepository.findByBarcode(ticketSellingPriceOfferDto.barcode)){ "해당 티켓 정보가 존재하지 않습니다." } @@ -45,7 +45,6 @@ class TicketService( ticketSellingPriceOfferDto.sellerName, ticketSellingPriceOfferDto.sellingPrice ) - ticketRepository.save(ticket) - + return ticketRepository.save(ticket) } } \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt index 5f6ed91..cb06c07 100644 --- a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt +++ b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt @@ -54,7 +54,9 @@ class Ticket( "공연 시작 1시간 전까지만 등록할 수 있습니다." } } - + fun getTicketStatus(): TicketStatus { + return ticketStatus + } fun applySellerOfferPrice(offerSellerName: String, offerPrice: BigDecimal) { require(sellerName == offerSellerName) { "티켓에 등록된 판매자가 아닙니다." } require(offerPrice >= BigDecimal.ZERO) { "판매가격에 음수는 입력할 수 없습니다." } diff --git a/src/main/kotlin/org/example/ticket/presentation/controller/TicketController.kt b/src/main/kotlin/org/example/ticket/presentation/controller/TicketController.kt new file mode 100644 index 0000000..7934e18 --- /dev/null +++ b/src/main/kotlin/org/example/ticket/presentation/controller/TicketController.kt @@ -0,0 +1,59 @@ +package org.example.ticket.presentation.controller + +import org.example.ticket.application.dto.TicketCreationDto +import org.example.ticket.application.dto.TicketResponseDto +import org.example.ticket.application.dto.TicketSellingPriceOfferDto +import org.example.ticket.application.service.TicketService.TicketService +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PatchMapping +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 + +@RestController +@RequestMapping("/tickets") +class TicketController( + private val ticketService: TicketService +) { + @PostMapping("/create") + fun createTicket(@RequestBody requestDto: TicketCreationDto): TicketResponseDto { + val ticket = ticketService.createTicket(requestDto) + return TicketResponseDto( + barcode = ticket.barcode, + seller = ticket.sellerName, + originalPrice = ticket.originalPrice, + sellingPrice = ticket.sellingPrice, + expirationDate = ticket.expirationDateTime, + ticketStatus = ticket.getTicketStatus(), + ) + } + + @PatchMapping("/price") + fun applySellerOfferPrice(@RequestBody dto: TicketSellingPriceOfferDto): TicketResponseDto { + val ticket = ticketService.applySellerOfferPrice(dto) + return TicketResponseDto( + barcode = ticket.barcode, + seller = ticket.sellerName, + originalPrice = ticket.originalPrice, + sellingPrice = ticket.sellingPrice, + expirationDate = ticket.expirationDateTime, + ticketStatus = ticket.getTicketStatus(), + ) + } + + @PostMapping("/resale") + fun reSaleTicket(@RequestBody dto: TicketSellingPriceOfferDto): TicketResponseDto { + val ticket = ticketService.reSaleTicket(dto) + return TicketResponseDto( + barcode = ticket.barcode, + seller = ticket.sellerName, + originalPrice = ticket.originalPrice, + sellingPrice = ticket.sellingPrice, + expirationDate = ticket.expirationDateTime, + ticketStatus = ticket.getTicketStatus(), + ) + } + + +} \ No newline at end of file From 8291c6652c64be29a2145126728d9b9cffee1016 Mon Sep 17 00:00:00 2001 From: songhansol Date: Fri, 27 Feb 2026 18:26:27 +0900 Subject: [PATCH 15/22] =?UTF-8?q?=EB=94=9C=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deal/application/dto/DealResponseDto.kt | 14 ++++++ .../presentation/controller/DealController.kt | 46 +++++++++++++++++++ .../deal/application/service/DealService.kt | 12 +++-- .../infrastructure/PaymentGatewayResolver.kt | 2 + 4 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/org/example/deal/application/dto/DealResponseDto.kt create mode 100644 src/main/kotlin/org/example/deal/application/presentation/controller/DealController.kt diff --git a/src/main/kotlin/org/example/deal/application/dto/DealResponseDto.kt b/src/main/kotlin/org/example/deal/application/dto/DealResponseDto.kt new file mode 100644 index 0000000..6e7d294 --- /dev/null +++ b/src/main/kotlin/org/example/deal/application/dto/DealResponseDto.kt @@ -0,0 +1,14 @@ +package org.example.deal.application.dto + +import org.example.deal.domain.enum.DealStatus +import java.math.BigDecimal +import java.time.LocalDateTime + +data class DealResponseDto( + val barcode: String, + val sellerName: String, + val buyerName: String, + val sellingPrice: BigDecimal, + val reservedDateTime: LocalDateTime, + val dealStatus: DealStatus +) diff --git a/src/main/kotlin/org/example/deal/application/presentation/controller/DealController.kt b/src/main/kotlin/org/example/deal/application/presentation/controller/DealController.kt new file mode 100644 index 0000000..696bfcc --- /dev/null +++ b/src/main/kotlin/org/example/deal/application/presentation/controller/DealController.kt @@ -0,0 +1,46 @@ +package org.example.deal.application.presentation.controller + +import org.example.deal.application.dto.DealEndDto +import org.example.deal.application.dto.DealResponseDto +import org.example.deal.application.dto.DealStartDto +import org.example.deal.application.service.DealService +import org.example.deal.domain.enum.DealStatus +import org.example.deal.domain.model.Deal +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.math.BigDecimal +import java.time.LocalDateTime +import kotlin.String + +@RestController +@RequestMapping(value = ["/deal"]) +class DealController ( + private val dealService: DealService +){ + @PostMapping("/start") + fun dealStart(@RequestBody dealStartDto: DealStartDto ): DealResponseDto { + val deal = dealService.dealStart(dealStartDto) + return DealResponseDto( + barcode = deal.barcode, + sellerName = deal.sellerName, + buyerName = deal.buyerName, + sellingPrice = deal.sellingPrice, + reservedDateTime = deal.reservedDateTime, + dealStatus = deal.getDealStatus() + ) + } + @PostMapping("/end") + fun dealEnd(@RequestBody dealEndDto: DealEndDto): DealResponseDto { + val deal = dealService.dealEnd(dealEndDto) + return DealResponseDto( + barcode = deal.barcode, + sellerName = deal.sellerName, + buyerName = deal.buyerName, + sellingPrice = deal.sellingPrice, + reservedDateTime = deal.reservedDateTime, + dealStatus = deal.getDealStatus() + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/example/deal/application/service/DealService.kt b/src/main/kotlin/org/example/deal/application/service/DealService.kt index 86c4e51..546afda 100644 --- a/src/main/kotlin/org/example/deal/application/service/DealService.kt +++ b/src/main/kotlin/org/example/deal/application/service/DealService.kt @@ -9,15 +9,16 @@ import org.example.deal.repository.DealJpaRepository import org.example.ticket.domain.model.Ticket import org.example.ticket.infra.repository.TicketJpaRepository import org.example.user.repository.UserJpaRepository +import org.springframework.stereotype.Service +@Service class DealService ( - private val userRepository: UserJpaRepository, private val dealRepository: DealJpaRepository, private val ticketRepository: TicketJpaRepository, private val paymentGatewayResolver: PaymentGatewayResolver ){ - fun dealStart(dealStartDto: DealStartDto){ + fun dealStart(dealStartDto: DealStartDto): Deal{ val ticket = requireNotNull(ticketRepository.findByBarcode(dealStartDto.barcode)){"존재하지 않는 티켓입니다."} val sellingPrice = requireNotNull(ticket.sellingPrice){"판매가가 설정되지 않았습니다."} @@ -29,10 +30,11 @@ class DealService ( ) ticket.ticketReserve(); - dealRepository.save(deal) + ticketRepository.save(ticket) + return dealRepository.save(deal) } - fun dealEnd(dealEndDto: DealEndDto){ + fun dealEnd(dealEndDto: DealEndDto): Deal{ val deal = requireNotNull(dealRepository.findByBarcode(dealEndDto.barcode)){"존재하지 않는 거래입니다."} val ticket = requireNotNull(ticketRepository.findByBarcode(dealEndDto.barcode)){"존재하지 않는 티켓입니다."} val dealExpiredCheck = deal.reservedTimeExpiredCheck(); @@ -45,6 +47,8 @@ class DealService ( paymentGateway.pay(deal.buyerName, deal.sellerName, deal.sellingPrice) deal.dealComplete() ticket.ticketSold() + ticketRepository.save(ticket) + return dealRepository.save(deal) } } \ No newline at end of file diff --git a/src/main/kotlin/org/example/deal/infrastructure/PaymentGatewayResolver.kt b/src/main/kotlin/org/example/deal/infrastructure/PaymentGatewayResolver.kt index 5bd6e72..e4e6436 100644 --- a/src/main/kotlin/org/example/deal/infrastructure/PaymentGatewayResolver.kt +++ b/src/main/kotlin/org/example/deal/infrastructure/PaymentGatewayResolver.kt @@ -1,7 +1,9 @@ package org.example.deal.infrastructure import org.example.deal.domain.enum.PaymentType +import org.springframework.stereotype.Component +@Component class PaymentGatewayResolver( private val gateways: List ) { From af45a9e902f6e5ff6888ee692ed891d904fe0b31 Mon Sep 17 00:00:00 2001 From: songhansol Date: Sun, 1 Mar 2026 19:32:23 +0900 Subject: [PATCH 16/22] PR --- .../presentation/controller/DealController.kt | 7 +++--- .../deal/application/service/DealService.kt | 18 +++++++-------- .../org/example/deal/domain/model/Deal.kt | 9 +++++--- .../infrastructure/KakaoPaymentGateway.kt | 3 ++- .../deal/infrastructure/PaymentGateway.kt | 4 ++-- .../deal/infrastructure/TossPaymentGateway.kt | 2 +- .../deal/repository/DealJpaRepository.kt | 2 +- .../error/contoroller/ErrorResponse.kt | 6 +++++ .../contoroller/GlobalExceptionHandller.kt | 22 +++++++++++++++++++ .../dto/TicketSellingPriceOfferDto.kt | 2 +- .../org/example/ticket/domain/model/Ticket.kt | 12 ++++++---- .../api/MelonTicketApiClient.kt | 3 ++- .../infrastructure/api/MolTicketApiClient.kt | 3 ++- .../infrastructure/api/NolTicketApiClient.kt | 1 + .../infrastructure/api/TicketApiClient.kt | 2 +- .../api/TicketApiClientResolver.kt | 4 ++-- .../dto/TicketApiResponseDto.kt | 2 +- .../repository/TicketJpaRepository.kt | 2 +- .../controller/TicketController.kt | 1 - .../org/example/user/domain/model/User.kt | 6 ++--- .../user/repository/UserJpaRepository.kt | 3 --- 21 files changed, 74 insertions(+), 40 deletions(-) create mode 100644 src/main/kotlin/org/example/error/contoroller/ErrorResponse.kt create mode 100644 src/main/kotlin/org/example/error/contoroller/GlobalExceptionHandller.kt diff --git a/src/main/kotlin/org/example/deal/application/presentation/controller/DealController.kt b/src/main/kotlin/org/example/deal/application/presentation/controller/DealController.kt index 696bfcc..4ae8893 100644 --- a/src/main/kotlin/org/example/deal/application/presentation/controller/DealController.kt +++ b/src/main/kotlin/org/example/deal/application/presentation/controller/DealController.kt @@ -16,11 +16,11 @@ import kotlin.String @RestController @RequestMapping(value = ["/deal"]) -class DealController ( +class DealController( private val dealService: DealService -){ +) { @PostMapping("/start") - fun dealStart(@RequestBody dealStartDto: DealStartDto ): DealResponseDto { + fun dealStart(@RequestBody dealStartDto: DealStartDto): DealResponseDto { val deal = dealService.dealStart(dealStartDto) return DealResponseDto( barcode = deal.barcode, @@ -31,6 +31,7 @@ class DealController ( dealStatus = deal.getDealStatus() ) } + @PostMapping("/end") fun dealEnd(@RequestBody dealEndDto: DealEndDto): DealResponseDto { val deal = dealService.dealEnd(dealEndDto) diff --git a/src/main/kotlin/org/example/deal/application/service/DealService.kt b/src/main/kotlin/org/example/deal/application/service/DealService.kt index 546afda..a692e5f 100644 --- a/src/main/kotlin/org/example/deal/application/service/DealService.kt +++ b/src/main/kotlin/org/example/deal/application/service/DealService.kt @@ -12,15 +12,15 @@ import org.example.user.repository.UserJpaRepository import org.springframework.stereotype.Service @Service -class DealService ( +class DealService( private val dealRepository: DealJpaRepository, private val ticketRepository: TicketJpaRepository, private val paymentGatewayResolver: PaymentGatewayResolver -){ +) { - fun dealStart(dealStartDto: DealStartDto): Deal{ - val ticket = requireNotNull(ticketRepository.findByBarcode(dealStartDto.barcode)){"존재하지 않는 티켓입니다."} - val sellingPrice = requireNotNull(ticket.sellingPrice){"판매가가 설정되지 않았습니다."} + fun dealStart(dealStartDto: DealStartDto): Deal { + val ticket = requireNotNull(ticketRepository.findByBarcode(dealStartDto.barcode)) { "존재하지 않는 티켓입니다." } + val sellingPrice = requireNotNull(ticket.sellingPrice) { "판매가가 설정되지 않았습니다." } val deal = Deal( barcode = ticket.barcode, @@ -34,11 +34,11 @@ class DealService ( return dealRepository.save(deal) } - fun dealEnd(dealEndDto: DealEndDto): Deal{ - val deal = requireNotNull(dealRepository.findByBarcode(dealEndDto.barcode)){"존재하지 않는 거래입니다."} - val ticket = requireNotNull(ticketRepository.findByBarcode(dealEndDto.barcode)){"존재하지 않는 티켓입니다."} + fun dealEnd(dealEndDto: DealEndDto): Deal { + val deal = requireNotNull(dealRepository.findByBarcode(dealEndDto.barcode)) { "존재하지 않는 거래입니다." } + val ticket = requireNotNull(ticketRepository.findByBarcode(dealEndDto.barcode)) { "존재하지 않는 티켓입니다." } val dealExpiredCheck = deal.reservedTimeExpiredCheck(); - if(dealExpiredCheck){ + if (dealExpiredCheck) { deal.dealCancel() ticket.ticketOnSale() throw IllegalArgumentException("10분 이내 입금이 되지않아 결제가 취소되었습니다.") diff --git a/src/main/kotlin/org/example/deal/domain/model/Deal.kt b/src/main/kotlin/org/example/deal/domain/model/Deal.kt index 2a0080b..0f8f336 100644 --- a/src/main/kotlin/org/example/deal/domain/model/Deal.kt +++ b/src/main/kotlin/org/example/deal/domain/model/Deal.kt @@ -25,18 +25,21 @@ class Deal( require(sellerName != buyerName) { "본인이 등록한 티켓은 구매할 수 없습니다." } } - fun dealComplete(){ + fun dealComplete() { require(dealStatus == DealStatus.RESERVED) { "예약 상태인 거래만 완료할 수 있습니다." } dealStatus = DealStatus.COMPLETED } - fun dealCancel(){ + + fun dealCancel() { require(dealStatus == DealStatus.RESERVED) { "예약 상태인 거래만 취소 할 수 있습니다." } dealStatus = DealStatus.CANCELLED } - fun reservedTimeExpiredCheck():Boolean{ + + fun reservedTimeExpiredCheck(): Boolean { return dealStatus == DealStatus.RESERVED && reservedDateTime.plusMinutes(10).isBefore(LocalDateTime.now()) } + fun getDealStatus(): DealStatus { return dealStatus } diff --git a/src/main/kotlin/org/example/deal/infrastructure/KakaoPaymentGateway.kt b/src/main/kotlin/org/example/deal/infrastructure/KakaoPaymentGateway.kt index 6c2c42c..135af09 100644 --- a/src/main/kotlin/org/example/deal/infrastructure/KakaoPaymentGateway.kt +++ b/src/main/kotlin/org/example/deal/infrastructure/KakaoPaymentGateway.kt @@ -7,13 +7,14 @@ import java.math.BigDecimal @Component -class KakaoPaymentGateway: PaymentGateway { +class KakaoPaymentGateway : PaymentGateway { override fun pay(buyerName: String, sellerName: String, price: BigDecimal) { val buyer = requireNotNull(UserJpaRepository.findByName(buyerName)) { "구매자 정보를 찾을 수 없습니다." } val seller = requireNotNull(UserJpaRepository.findByName(sellerName)) { "판매자 정보를 찾을 수 없습니다." } buyer.withdraw(price) seller.deposit(price) } + override fun type(): PaymentType { return PaymentType.KAKAO } diff --git a/src/main/kotlin/org/example/deal/infrastructure/PaymentGateway.kt b/src/main/kotlin/org/example/deal/infrastructure/PaymentGateway.kt index b4936e3..5c30f31 100644 --- a/src/main/kotlin/org/example/deal/infrastructure/PaymentGateway.kt +++ b/src/main/kotlin/org/example/deal/infrastructure/PaymentGateway.kt @@ -3,7 +3,7 @@ package org.example.deal.infrastructure import org.example.deal.domain.enum.PaymentType import java.math.BigDecimal -interface PaymentGateway{ - fun pay(buyerName:String, sellerName:String, price: BigDecimal) +interface PaymentGateway { + fun pay(buyerName: String, sellerName: String, price: BigDecimal) fun type(): PaymentType } \ No newline at end of file diff --git a/src/main/kotlin/org/example/deal/infrastructure/TossPaymentGateway.kt b/src/main/kotlin/org/example/deal/infrastructure/TossPaymentGateway.kt index 8236ce4..5623ccb 100644 --- a/src/main/kotlin/org/example/deal/infrastructure/TossPaymentGateway.kt +++ b/src/main/kotlin/org/example/deal/infrastructure/TossPaymentGateway.kt @@ -6,7 +6,7 @@ import org.springframework.stereotype.Component import java.math.BigDecimal @Component -class TossPaymentGateway: PaymentGateway { +class TossPaymentGateway : PaymentGateway { override fun pay(buyerName: String, sellerName: String, price: BigDecimal) { val buyer = requireNotNull(UserJpaRepository.findByName(buyerName)) { "구매자 정보를 찾을 수 없습니다." } val seller = requireNotNull(UserJpaRepository.findByName(sellerName)) { "판매자 정보를 찾을 수 없습니다." } diff --git a/src/main/kotlin/org/example/deal/repository/DealJpaRepository.kt b/src/main/kotlin/org/example/deal/repository/DealJpaRepository.kt index 7143dc4..b7149f4 100644 --- a/src/main/kotlin/org/example/deal/repository/DealJpaRepository.kt +++ b/src/main/kotlin/org/example/deal/repository/DealJpaRepository.kt @@ -3,7 +3,7 @@ package org.example.deal.repository import org.example.deal.domain.model.Deal import org.springframework.data.jpa.repository.JpaRepository -interface DealJpaRepository: JpaRepository { +interface DealJpaRepository : JpaRepository { fun findByBarcode(barcode: String): Deal? fun findBySellerName(sellerName: String): Deal? } diff --git a/src/main/kotlin/org/example/error/contoroller/ErrorResponse.kt b/src/main/kotlin/org/example/error/contoroller/ErrorResponse.kt new file mode 100644 index 0000000..7e7d2a6 --- /dev/null +++ b/src/main/kotlin/org/example/error/contoroller/ErrorResponse.kt @@ -0,0 +1,6 @@ +package org.example.error.contoroller + +data class ErrorResponse( + val message: String, + val status: Int +) \ No newline at end of file diff --git a/src/main/kotlin/org/example/error/contoroller/GlobalExceptionHandller.kt b/src/main/kotlin/org/example/error/contoroller/GlobalExceptionHandller.kt new file mode 100644 index 0000000..8b10063 --- /dev/null +++ b/src/main/kotlin/org/example/error/contoroller/GlobalExceptionHandller.kt @@ -0,0 +1,22 @@ +package org.example.error.contoroller + +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice + +@RestControllerAdvice +class GlobalExceptionHandler { + + @ExceptionHandler(IllegalArgumentException::class) + fun handleIllegalArgument(e: IllegalArgumentException): ResponseEntity { + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body( + ErrorResponse( + message = e.message ?: "잘못된 요청입니다.", + status = 400 + ) + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/application/dto/TicketSellingPriceOfferDto.kt b/src/main/kotlin/org/example/ticket/application/dto/TicketSellingPriceOfferDto.kt index b61e198..98339c9 100644 --- a/src/main/kotlin/org/example/ticket/application/dto/TicketSellingPriceOfferDto.kt +++ b/src/main/kotlin/org/example/ticket/application/dto/TicketSellingPriceOfferDto.kt @@ -6,4 +6,4 @@ data class TicketSellingPriceOfferDto( val sellerName: String, val barcode: String, val sellingPrice: BigDecimal, -) +) \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt index cb06c07..dbc00cb 100644 --- a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt +++ b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt @@ -44,8 +44,8 @@ class Ticket( } fun getTicketDeadLine(): LocalDateTime { - return expirationDateTime.minusHours(1); - }; + return expirationDateTime.minusHours(1) + } fun ticketTimeCheck(performanceDateTime: LocalDateTime) { val now = LocalDateTime.now() @@ -54,13 +54,16 @@ class Ticket( "공연 시작 1시간 전까지만 등록할 수 있습니다." } } + fun getTicketStatus(): TicketStatus { return ticketStatus } + fun applySellerOfferPrice(offerSellerName: String, offerPrice: BigDecimal) { require(sellerName == offerSellerName) { "티켓에 등록된 판매자가 아닙니다." } require(offerPrice >= BigDecimal.ZERO) { "판매가격에 음수는 입력할 수 없습니다." } require(offerPrice <= originalPrice) { "티켓의 가격은 정가를 초과할 수 없습니다." } + require(ticketStatus == TicketStatus.ON_SALE) { "판매중인 제품만 가격을 수정할 수 있습니다." } val isPerformanceDateTime = LocalDateTime.now().toLocalDate() == expirationDateTime.toLocalDate() if (isPerformanceDateTime) { val maxAllowedPrice = originalPrice.multiply(HALF) @@ -69,10 +72,11 @@ class Ticket( sellingPrice = offerPrice } + fun reSale(offerSellerName: String, offerPrice: BigDecimal) { require(ticketStatus == TicketStatus.SOLD) { "구매한 티켓만 판매할 수 있습니다." } ticketStatus = TicketStatus.ON_SALE - applySellerOfferPrice(offerSellerName, offerPrice); + applySellerOfferPrice(offerSellerName, offerPrice) } fun ticketOnSale() { @@ -81,7 +85,7 @@ class Ticket( } fun ticketReserve() { - require(ticketStatus == TicketStatus.ON_SALE) { "판매중인 티켓만 예약할 수 있습니다" } + require(ticketStatus == TicketStatus.ON_SALE) { "판매중인 티켓만 예약할 수 있습니다" } ticketStatus = TicketStatus.RESERVED } diff --git a/src/main/kotlin/org/example/ticket/infrastructure/api/MelonTicketApiClient.kt b/src/main/kotlin/org/example/ticket/infrastructure/api/MelonTicketApiClient.kt index df61c4c..b857f42 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/api/MelonTicketApiClient.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/api/MelonTicketApiClient.kt @@ -7,7 +7,7 @@ import java.math.BigDecimal import java.time.LocalDateTime @Component -class MelonTicketApiClient : TicketApiClient{ +class MelonTicketApiClient : TicketApiClient { override fun getTicket(barcode: String): TicketApiResponseDto { return TicketApiResponseDto( @@ -15,6 +15,7 @@ class MelonTicketApiClient : TicketApiClient{ price = BigDecimal.valueOf(10000) ) } + override fun type(): TicketType { return TicketType.MELON } diff --git a/src/main/kotlin/org/example/ticket/infrastructure/api/MolTicketApiClient.kt b/src/main/kotlin/org/example/ticket/infrastructure/api/MolTicketApiClient.kt index c4e9e56..57b0238 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/api/MolTicketApiClient.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/api/MolTicketApiClient.kt @@ -7,13 +7,14 @@ import java.math.BigDecimal import java.time.LocalDateTime @Component -class MolTicketApiClient: TicketApiClient { +class MolTicketApiClient : TicketApiClient { override fun getTicket(barcode: String): TicketApiResponseDto { return TicketApiResponseDto( performanceDateTime = LocalDateTime.now().plusDays(5), price = BigDecimal.valueOf(30000) ) } + override fun type(): TicketType { return TicketType.MOL } diff --git a/src/main/kotlin/org/example/ticket/infrastructure/api/NolTicketApiClient.kt b/src/main/kotlin/org/example/ticket/infrastructure/api/NolTicketApiClient.kt index 0508503..050f582 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/api/NolTicketApiClient.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/api/NolTicketApiClient.kt @@ -14,6 +14,7 @@ class NolTicketApiClient : TicketApiClient { price = BigDecimal.valueOf(20000) ) } + override fun type(): TicketType { return TicketType.NOL } diff --git a/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClient.kt b/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClient.kt index ae56389..0992793 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClient.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClient.kt @@ -5,6 +5,6 @@ import org.example.ticket.infrastructure.dto.TicketApiResponseDto interface TicketApiClient { fun getTicket(barcode: String): TicketApiResponseDto - fun type() : TicketType + fun type(): TicketType } \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClientResolver.kt b/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClientResolver.kt index c01cad6..6488c69 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClientResolver.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClientResolver.kt @@ -4,9 +4,9 @@ import org.example.ticket.domain.model.Ticket import org.springframework.stereotype.Component @Component -class TicketApiClientResolver ( +class TicketApiClientResolver( private val ticketApiClients: List -){ +) { fun resolve(barcode: String): TicketApiClient { val ticketType = Ticket.ticketTypeCheck(barcode) return ticketApiClients.first { it.type() == ticketType } diff --git a/src/main/kotlin/org/example/ticket/infrastructure/dto/TicketApiResponseDto.kt b/src/main/kotlin/org/example/ticket/infrastructure/dto/TicketApiResponseDto.kt index 8bdbecf..b2a1b61 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/dto/TicketApiResponseDto.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/dto/TicketApiResponseDto.kt @@ -9,7 +9,7 @@ data class TicketApiResponseDto( val performanceDateTime: LocalDateTime, val price: BigDecimal ) { - fun toTicket(barcode: String, sellerId: String, ticketType: TicketType,): Ticket { + fun toTicket(barcode: String, sellerId: String, ticketType: TicketType): Ticket { return Ticket( barcode = barcode, ticketType = ticketType, diff --git a/src/main/kotlin/org/example/ticket/infrastructure/repository/TicketJpaRepository.kt b/src/main/kotlin/org/example/ticket/infrastructure/repository/TicketJpaRepository.kt index 6284124..ce25c9d 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/repository/TicketJpaRepository.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/repository/TicketJpaRepository.kt @@ -3,7 +3,7 @@ package org.example.ticket.infra.repository import org.example.ticket.domain.model.Ticket import org.springframework.data.jpa.repository.JpaRepository -interface TicketJpaRepository: JpaRepository { +interface TicketJpaRepository : JpaRepository { fun findByBarcode(barcode: String): Ticket? // ? 넣어야 null 처리되서 조회 안될때 에러가 안남 } diff --git a/src/main/kotlin/org/example/ticket/presentation/controller/TicketController.kt b/src/main/kotlin/org/example/ticket/presentation/controller/TicketController.kt index 7934e18..f4f6a5a 100644 --- a/src/main/kotlin/org/example/ticket/presentation/controller/TicketController.kt +++ b/src/main/kotlin/org/example/ticket/presentation/controller/TicketController.kt @@ -55,5 +55,4 @@ class TicketController( ) } - } \ No newline at end of file diff --git a/src/main/kotlin/org/example/user/domain/model/User.kt b/src/main/kotlin/org/example/user/domain/model/User.kt index 51b92b8..6a1073b 100644 --- a/src/main/kotlin/org/example/user/domain/model/User.kt +++ b/src/main/kotlin/org/example/user/domain/model/User.kt @@ -4,14 +4,12 @@ package org.example.user.domain.model import org.example.user.domain.enum.UserRole import java.math.BigDecimal -class User ( +class User( val id: Long? = null, val name: String, val role: UserRole, var money: BigDecimal = BigDecimal.ZERO, -){ - companion object{} - init {} +) { fun deposit(amount: BigDecimal) { require(amount > BigDecimal.ZERO) { "입금액은 0보다 커야 합니다." } money = money.add(amount) diff --git a/src/main/kotlin/org/example/user/repository/UserJpaRepository.kt b/src/main/kotlin/org/example/user/repository/UserJpaRepository.kt index a3b75d9..d2afec0 100644 --- a/src/main/kotlin/org/example/user/repository/UserJpaRepository.kt +++ b/src/main/kotlin/org/example/user/repository/UserJpaRepository.kt @@ -13,7 +13,4 @@ object UserJpaRepository { ) fun findByName(name: String): User? = users.find { it.name == name } - - - } \ No newline at end of file From cab7a30180e2cca37b249c9be2c90b5a12b96c24 Mon Sep 17 00:00:00 2001 From: songhansol Date: Wed, 4 Mar 2026 18:57:51 +0900 Subject: [PATCH 17/22] PR --- .../presentation/controller/DealController.kt | 47 ------------ .../deal/application/service/DealService.kt | 40 +++++++---- .../{ => api}/KakaoPaymentGateway.kt | 2 +- .../{ => api}/PaymentGateway.kt | 2 +- .../{ => api}/PaymentGatewayResolver.kt | 2 +- .../{ => api}/TossPaymentGateway.kt | 2 +- .../repository/DealJpaRepository.kt | 2 +- .../presentation/controller/DealController.kt | 26 +++++++ .../ErrorResponse.kt | 2 +- .../GlobalExceptionHandller.kt | 2 +- .../application/service/TicketService.kt | 72 +++++++++++++++++++ .../service/TicketService/TicketService.kt | 50 ------------- .../repository/TicketJpaRepository.kt | 2 +- .../controller/TicketController.kt | 34 ++------- 14 files changed, 138 insertions(+), 147 deletions(-) delete mode 100644 src/main/kotlin/org/example/deal/application/presentation/controller/DealController.kt rename src/main/kotlin/org/example/deal/infrastructure/{ => api}/KakaoPaymentGateway.kt (94%) rename src/main/kotlin/org/example/deal/infrastructure/{ => api}/PaymentGateway.kt (82%) rename src/main/kotlin/org/example/deal/infrastructure/{ => api}/PaymentGatewayResolver.kt (90%) rename src/main/kotlin/org/example/deal/infrastructure/{ => api}/TossPaymentGateway.kt (94%) rename src/main/kotlin/org/example/deal/{ => infrastructure}/repository/DealJpaRepository.kt (83%) create mode 100644 src/main/kotlin/org/example/deal/presentation/controller/DealController.kt rename src/main/kotlin/org/example/error/{contoroller => controller}/ErrorResponse.kt (65%) rename src/main/kotlin/org/example/error/{contoroller => controller}/GlobalExceptionHandller.kt (94%) create mode 100644 src/main/kotlin/org/example/ticket/application/service/TicketService.kt delete mode 100644 src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt diff --git a/src/main/kotlin/org/example/deal/application/presentation/controller/DealController.kt b/src/main/kotlin/org/example/deal/application/presentation/controller/DealController.kt deleted file mode 100644 index 4ae8893..0000000 --- a/src/main/kotlin/org/example/deal/application/presentation/controller/DealController.kt +++ /dev/null @@ -1,47 +0,0 @@ -package org.example.deal.application.presentation.controller - -import org.example.deal.application.dto.DealEndDto -import org.example.deal.application.dto.DealResponseDto -import org.example.deal.application.dto.DealStartDto -import org.example.deal.application.service.DealService -import org.example.deal.domain.enum.DealStatus -import org.example.deal.domain.model.Deal -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.math.BigDecimal -import java.time.LocalDateTime -import kotlin.String - -@RestController -@RequestMapping(value = ["/deal"]) -class DealController( - private val dealService: DealService -) { - @PostMapping("/start") - fun dealStart(@RequestBody dealStartDto: DealStartDto): DealResponseDto { - val deal = dealService.dealStart(dealStartDto) - return DealResponseDto( - barcode = deal.barcode, - sellerName = deal.sellerName, - buyerName = deal.buyerName, - sellingPrice = deal.sellingPrice, - reservedDateTime = deal.reservedDateTime, - dealStatus = deal.getDealStatus() - ) - } - - @PostMapping("/end") - fun dealEnd(@RequestBody dealEndDto: DealEndDto): DealResponseDto { - val deal = dealService.dealEnd(dealEndDto) - return DealResponseDto( - barcode = deal.barcode, - sellerName = deal.sellerName, - buyerName = deal.buyerName, - sellingPrice = deal.sellingPrice, - reservedDateTime = deal.reservedDateTime, - dealStatus = deal.getDealStatus() - ) - } -} \ No newline at end of file diff --git a/src/main/kotlin/org/example/deal/application/service/DealService.kt b/src/main/kotlin/org/example/deal/application/service/DealService.kt index a692e5f..8b6a8ad 100644 --- a/src/main/kotlin/org/example/deal/application/service/DealService.kt +++ b/src/main/kotlin/org/example/deal/application/service/DealService.kt @@ -1,14 +1,12 @@ package org.example.deal.application.service import org.example.deal.application.dto.DealEndDto +import org.example.deal.application.dto.DealResponseDto import org.example.deal.application.dto.DealStartDto import org.example.deal.domain.model.Deal -import org.example.deal.infrastructure.KakaoPaymentGateway -import org.example.deal.infrastructure.PaymentGatewayResolver -import org.example.deal.repository.DealJpaRepository -import org.example.ticket.domain.model.Ticket -import org.example.ticket.infra.repository.TicketJpaRepository -import org.example.user.repository.UserJpaRepository +import org.example.deal.infrastructure.api.PaymentGatewayResolver +import org.example.deal.infrastructure.repository.DealJpaRepository +import org.example.ticket.infrastructure.repository.TicketJpaRepository import org.springframework.stereotype.Service @Service @@ -18,7 +16,7 @@ class DealService( private val paymentGatewayResolver: PaymentGatewayResolver ) { - fun dealStart(dealStartDto: DealStartDto): Deal { + fun dealStart(dealStartDto: DealStartDto): DealResponseDto { val ticket = requireNotNull(ticketRepository.findByBarcode(dealStartDto.barcode)) { "존재하지 않는 티켓입니다." } val sellingPrice = requireNotNull(ticket.sellingPrice) { "판매가가 설정되지 않았습니다." } @@ -29,18 +27,28 @@ class DealService( sellingPrice = sellingPrice ) - ticket.ticketReserve(); + ticket.ticketReserve() ticketRepository.save(ticket) - return dealRepository.save(deal) + val savedDeal = dealRepository.save(deal) + return DealResponseDto( + barcode = savedDeal.barcode, + sellerName = savedDeal.sellerName, + buyerName = savedDeal.buyerName, + sellingPrice = savedDeal.sellingPrice, + reservedDateTime = savedDeal.reservedDateTime, + dealStatus = savedDeal.getDealStatus() + ) } - fun dealEnd(dealEndDto: DealEndDto): Deal { + fun dealEnd(dealEndDto: DealEndDto): DealResponseDto { val deal = requireNotNull(dealRepository.findByBarcode(dealEndDto.barcode)) { "존재하지 않는 거래입니다." } val ticket = requireNotNull(ticketRepository.findByBarcode(dealEndDto.barcode)) { "존재하지 않는 티켓입니다." } - val dealExpiredCheck = deal.reservedTimeExpiredCheck(); + val dealExpiredCheck = deal.reservedTimeExpiredCheck() if (dealExpiredCheck) { deal.dealCancel() ticket.ticketOnSale() + ticketRepository.save(ticket) + dealRepository.save(deal) throw IllegalArgumentException("10분 이내 입금이 되지않아 결제가 취소되었습니다.") } val paymentGateway = paymentGatewayResolver.resolve(dealEndDto.payementType) @@ -48,7 +56,15 @@ class DealService( deal.dealComplete() ticket.ticketSold() ticketRepository.save(ticket) - return dealRepository.save(deal) + val savedDeal = dealRepository.save(deal) + return DealResponseDto( + barcode = savedDeal.barcode, + sellerName = savedDeal.sellerName, + buyerName = savedDeal.buyerName, + sellingPrice = savedDeal.sellingPrice, + reservedDateTime = savedDeal.reservedDateTime, + dealStatus = savedDeal.getDealStatus() + ) } } \ No newline at end of file diff --git a/src/main/kotlin/org/example/deal/infrastructure/KakaoPaymentGateway.kt b/src/main/kotlin/org/example/deal/infrastructure/api/KakaoPaymentGateway.kt similarity index 94% rename from src/main/kotlin/org/example/deal/infrastructure/KakaoPaymentGateway.kt rename to src/main/kotlin/org/example/deal/infrastructure/api/KakaoPaymentGateway.kt index 135af09..51b7e25 100644 --- a/src/main/kotlin/org/example/deal/infrastructure/KakaoPaymentGateway.kt +++ b/src/main/kotlin/org/example/deal/infrastructure/api/KakaoPaymentGateway.kt @@ -1,4 +1,4 @@ -package org.example.deal.infrastructure +package org.example.deal.infrastructure.api import org.example.deal.domain.enum.PaymentType import org.springframework.stereotype.Component diff --git a/src/main/kotlin/org/example/deal/infrastructure/PaymentGateway.kt b/src/main/kotlin/org/example/deal/infrastructure/api/PaymentGateway.kt similarity index 82% rename from src/main/kotlin/org/example/deal/infrastructure/PaymentGateway.kt rename to src/main/kotlin/org/example/deal/infrastructure/api/PaymentGateway.kt index 5c30f31..1ba2416 100644 --- a/src/main/kotlin/org/example/deal/infrastructure/PaymentGateway.kt +++ b/src/main/kotlin/org/example/deal/infrastructure/api/PaymentGateway.kt @@ -1,4 +1,4 @@ -package org.example.deal.infrastructure +package org.example.deal.infrastructure.api import org.example.deal.domain.enum.PaymentType import java.math.BigDecimal diff --git a/src/main/kotlin/org/example/deal/infrastructure/PaymentGatewayResolver.kt b/src/main/kotlin/org/example/deal/infrastructure/api/PaymentGatewayResolver.kt similarity index 90% rename from src/main/kotlin/org/example/deal/infrastructure/PaymentGatewayResolver.kt rename to src/main/kotlin/org/example/deal/infrastructure/api/PaymentGatewayResolver.kt index e4e6436..27488e1 100644 --- a/src/main/kotlin/org/example/deal/infrastructure/PaymentGatewayResolver.kt +++ b/src/main/kotlin/org/example/deal/infrastructure/api/PaymentGatewayResolver.kt @@ -1,4 +1,4 @@ -package org.example.deal.infrastructure +package org.example.deal.infrastructure.api import org.example.deal.domain.enum.PaymentType import org.springframework.stereotype.Component diff --git a/src/main/kotlin/org/example/deal/infrastructure/TossPaymentGateway.kt b/src/main/kotlin/org/example/deal/infrastructure/api/TossPaymentGateway.kt similarity index 94% rename from src/main/kotlin/org/example/deal/infrastructure/TossPaymentGateway.kt rename to src/main/kotlin/org/example/deal/infrastructure/api/TossPaymentGateway.kt index 5623ccb..8f43b1f 100644 --- a/src/main/kotlin/org/example/deal/infrastructure/TossPaymentGateway.kt +++ b/src/main/kotlin/org/example/deal/infrastructure/api/TossPaymentGateway.kt @@ -1,4 +1,4 @@ -package org.example.deal.infrastructure +package org.example.deal.infrastructure.api import org.example.deal.domain.enum.PaymentType import org.example.user.repository.UserJpaRepository diff --git a/src/main/kotlin/org/example/deal/repository/DealJpaRepository.kt b/src/main/kotlin/org/example/deal/infrastructure/repository/DealJpaRepository.kt similarity index 83% rename from src/main/kotlin/org/example/deal/repository/DealJpaRepository.kt rename to src/main/kotlin/org/example/deal/infrastructure/repository/DealJpaRepository.kt index b7149f4..427d6e0 100644 --- a/src/main/kotlin/org/example/deal/repository/DealJpaRepository.kt +++ b/src/main/kotlin/org/example/deal/infrastructure/repository/DealJpaRepository.kt @@ -1,4 +1,4 @@ -package org.example.deal.repository +package org.example.deal.infrastructure.repository import org.example.deal.domain.model.Deal import org.springframework.data.jpa.repository.JpaRepository diff --git a/src/main/kotlin/org/example/deal/presentation/controller/DealController.kt b/src/main/kotlin/org/example/deal/presentation/controller/DealController.kt new file mode 100644 index 0000000..07d7394 --- /dev/null +++ b/src/main/kotlin/org/example/deal/presentation/controller/DealController.kt @@ -0,0 +1,26 @@ +package org.example.deal.presentation.controller + +import org.example.deal.application.dto.DealEndDto +import org.example.deal.application.dto.DealResponseDto +import org.example.deal.application.dto.DealStartDto +import org.example.deal.application.service.DealService +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 + +@RestController +@RequestMapping(value = ["/deal"]) +class DealController( + private val dealService: DealService +) { + @PostMapping("/start") + fun dealStart(@RequestBody dealStartDto: DealStartDto): DealResponseDto { + return dealService.dealStart(dealStartDto) + } + + @PostMapping("/end") + fun dealEnd(@RequestBody dealEndDto: DealEndDto): DealResponseDto { + return dealService.dealEnd(dealEndDto) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/example/error/contoroller/ErrorResponse.kt b/src/main/kotlin/org/example/error/controller/ErrorResponse.kt similarity index 65% rename from src/main/kotlin/org/example/error/contoroller/ErrorResponse.kt rename to src/main/kotlin/org/example/error/controller/ErrorResponse.kt index 7e7d2a6..6da70fe 100644 --- a/src/main/kotlin/org/example/error/contoroller/ErrorResponse.kt +++ b/src/main/kotlin/org/example/error/controller/ErrorResponse.kt @@ -1,4 +1,4 @@ -package org.example.error.contoroller +package org.example.error.controller data class ErrorResponse( val message: String, diff --git a/src/main/kotlin/org/example/error/contoroller/GlobalExceptionHandller.kt b/src/main/kotlin/org/example/error/controller/GlobalExceptionHandller.kt similarity index 94% rename from src/main/kotlin/org/example/error/contoroller/GlobalExceptionHandller.kt rename to src/main/kotlin/org/example/error/controller/GlobalExceptionHandller.kt index 8b10063..825056a 100644 --- a/src/main/kotlin/org/example/error/contoroller/GlobalExceptionHandller.kt +++ b/src/main/kotlin/org/example/error/controller/GlobalExceptionHandller.kt @@ -1,4 +1,4 @@ -package org.example.error.contoroller +package org.example.error.controller import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity diff --git a/src/main/kotlin/org/example/ticket/application/service/TicketService.kt b/src/main/kotlin/org/example/ticket/application/service/TicketService.kt new file mode 100644 index 0000000..e16d332 --- /dev/null +++ b/src/main/kotlin/org/example/ticket/application/service/TicketService.kt @@ -0,0 +1,72 @@ +package org.example.ticket.application.service + +import org.example.ticket.application.dto.TicketCreationDto +import org.example.ticket.application.dto.TicketResponseDto +import org.example.ticket.application.dto.TicketSellingPriceOfferDto +import org.example.ticket.infrastructure.repository.TicketJpaRepository +import org.example.ticket.infrastructure.api.TicketApiClientResolver +import org.springframework.stereotype.Service + +@Service +class TicketService( + private val ticketApiClientResolver: TicketApiClientResolver, + private val ticketRepository: TicketJpaRepository +) { + fun createTicket(ticketCreationDto: TicketCreationDto): TicketResponseDto { + val apiClient = ticketApiClientResolver.resolve(ticketCreationDto.barcode) + val ticketResponseDto = apiClient.getTicket(ticketCreationDto.barcode) + val ticket = ticketRepository.save( + ticketResponseDto.toTicket( + ticketCreationDto.barcode, + ticketCreationDto.sellerName, + apiClient.type(), + ) + ) + return TicketResponseDto( + barcode = ticket.barcode, + seller = ticket.sellerName, + originalPrice = ticket.originalPrice, + sellingPrice = ticket.sellingPrice, + expirationDate = ticket.expirationDateTime, + ticketStatus = ticket.getTicketStatus(), + ) + } + + fun applySellerOfferPrice(ticketSellingPriceOfferDto: TicketSellingPriceOfferDto): TicketResponseDto { + val ticket = requireNotNull(ticketRepository.findByBarcode(ticketSellingPriceOfferDto.barcode)){ + "해당 티켓 정보가 존재하지 않습니다." + } + ticket.applySellerOfferPrice( + ticketSellingPriceOfferDto.sellerName, + ticketSellingPriceOfferDto.sellingPrice + ) + ticketRepository.save(ticket) + return TicketResponseDto( + barcode = ticket.barcode, + seller = ticket.sellerName, + originalPrice = ticket.originalPrice, + sellingPrice = ticket.sellingPrice, + expirationDate = ticket.expirationDateTime, + ticketStatus = ticket.getTicketStatus(), + ) + } + + fun reSaleTicket(ticketSellingPriceOfferDto: TicketSellingPriceOfferDto): TicketResponseDto { + val ticket = requireNotNull(ticketRepository.findByBarcode(ticketSellingPriceOfferDto.barcode)){ + "해당 티켓 정보가 존재하지 않습니다." + } + ticket.reSale( + ticketSellingPriceOfferDto.sellerName, + ticketSellingPriceOfferDto.sellingPrice + ) + ticketRepository.save(ticket) + return TicketResponseDto( + barcode = ticket.barcode, + seller = ticket.sellerName, + originalPrice = ticket.originalPrice, + sellingPrice = ticket.sellingPrice, + expirationDate = ticket.expirationDateTime, + ticketStatus = ticket.getTicketStatus(), + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt b/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt deleted file mode 100644 index 6e7e43a..0000000 --- a/src/main/kotlin/org/example/ticket/application/service/TicketService/TicketService.kt +++ /dev/null @@ -1,50 +0,0 @@ -package org.example.ticket.application.service.TicketService - -import org.example.ticket.application.dto.TicketCreationDto -import org.example.ticket.application.dto.TicketSellingPriceOfferDto -import org.example.ticket.domain.enum.TicketStatus -import org.example.ticket.domain.model.Ticket -import org.example.ticket.infra.repository.TicketJpaRepository -import org.example.ticket.infrastructure.api.TicketApiClient -import org.example.ticket.infrastructure.api.TicketApiClientResolver -import org.springframework.stereotype.Service -import java.math.BigDecimal - -@Service -class TicketService( - private val ticketApiClientResolver: TicketApiClientResolver, - private val ticketRepository: TicketJpaRepository -) { - fun createTicket(ticketCreationDto: TicketCreationDto): Ticket { - val apiClient = ticketApiClientResolver.resolve(ticketCreationDto.barcode) - val ticketResponseDto = apiClient.getTicket(ticketCreationDto.barcode) - val ticket = ticketResponseDto.toTicket( - ticketCreationDto.barcode, - ticketCreationDto.sellerName, - apiClient.type(), - ) - return ticketRepository.save(ticket) - }; - - fun applySellerOfferPrice(ticketSellingPriceOfferDto: TicketSellingPriceOfferDto): Ticket { - val ticket = requireNotNull(ticketRepository.findByBarcode(ticketSellingPriceOfferDto.barcode)){ - "해당 티켓 정보가 존재하지 않습니다." - } - ticket.applySellerOfferPrice( - ticketSellingPriceOfferDto.sellerName, - ticketSellingPriceOfferDto.sellingPrice - ) - return ticketRepository.save(ticket) - } - - fun reSaleTicket(ticketSellingPriceOfferDto: TicketSellingPriceOfferDto): Ticket { - val ticket = requireNotNull(ticketRepository.findByBarcode(ticketSellingPriceOfferDto.barcode)){ - "해당 티켓 정보가 존재하지 않습니다." - } - ticket.reSale( - ticketSellingPriceOfferDto.sellerName, - ticketSellingPriceOfferDto.sellingPrice - ) - return ticketRepository.save(ticket) - } -} \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/infrastructure/repository/TicketJpaRepository.kt b/src/main/kotlin/org/example/ticket/infrastructure/repository/TicketJpaRepository.kt index ce25c9d..77a14d8 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/repository/TicketJpaRepository.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/repository/TicketJpaRepository.kt @@ -1,4 +1,4 @@ -package org.example.ticket.infra.repository +package org.example.ticket.infrastructure.repository import org.example.ticket.domain.model.Ticket import org.springframework.data.jpa.repository.JpaRepository diff --git a/src/main/kotlin/org/example/ticket/presentation/controller/TicketController.kt b/src/main/kotlin/org/example/ticket/presentation/controller/TicketController.kt index f4f6a5a..c245c70 100644 --- a/src/main/kotlin/org/example/ticket/presentation/controller/TicketController.kt +++ b/src/main/kotlin/org/example/ticket/presentation/controller/TicketController.kt @@ -3,8 +3,7 @@ package org.example.ticket.presentation.controller import org.example.ticket.application.dto.TicketCreationDto import org.example.ticket.application.dto.TicketResponseDto import org.example.ticket.application.dto.TicketSellingPriceOfferDto -import org.example.ticket.application.service.TicketService.TicketService -import org.springframework.http.ResponseEntity +import org.example.ticket.application.service.TicketService import org.springframework.web.bind.annotation.PatchMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody @@ -18,41 +17,16 @@ class TicketController( ) { @PostMapping("/create") fun createTicket(@RequestBody requestDto: TicketCreationDto): TicketResponseDto { - val ticket = ticketService.createTicket(requestDto) - return TicketResponseDto( - barcode = ticket.barcode, - seller = ticket.sellerName, - originalPrice = ticket.originalPrice, - sellingPrice = ticket.sellingPrice, - expirationDate = ticket.expirationDateTime, - ticketStatus = ticket.getTicketStatus(), - ) + return ticketService.createTicket(requestDto) } @PatchMapping("/price") fun applySellerOfferPrice(@RequestBody dto: TicketSellingPriceOfferDto): TicketResponseDto { - val ticket = ticketService.applySellerOfferPrice(dto) - return TicketResponseDto( - barcode = ticket.barcode, - seller = ticket.sellerName, - originalPrice = ticket.originalPrice, - sellingPrice = ticket.sellingPrice, - expirationDate = ticket.expirationDateTime, - ticketStatus = ticket.getTicketStatus(), - ) + return ticketService.applySellerOfferPrice(dto) } @PostMapping("/resale") fun reSaleTicket(@RequestBody dto: TicketSellingPriceOfferDto): TicketResponseDto { - val ticket = ticketService.reSaleTicket(dto) - return TicketResponseDto( - barcode = ticket.barcode, - seller = ticket.sellerName, - originalPrice = ticket.originalPrice, - sellingPrice = ticket.sellingPrice, - expirationDate = ticket.expirationDateTime, - ticketStatus = ticket.getTicketStatus(), - ) + return ticketService.reSaleTicket(dto) } - } \ No newline at end of file From 50575a6ffa1205156c1931e1a7c0bd16489e3cc6 Mon Sep 17 00:00:00 2001 From: songhansol Date: Mon, 9 Mar 2026 18:21:07 +0900 Subject: [PATCH 18/22] =?UTF-8?q?=EC=98=A4=ED=83=80=EC=88=98=EC=A0=95=20ge?= =?UTF-8?q?tter=20=EB=8C=80=EC=8B=A0=20private=20set=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=20=ED=8B=B0=EC=BC=93=ED=83=80=EC=9E=85=EC=B2=B4=ED=81=AC=20mod?= =?UTF-8?q?el=EC=97=90=EC=84=9C=20ApiClientResolver=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deal/application/dto/DealEndDto.kt | 2 +- .../deal/application/service/DealService.kt | 7 +++-- .../org/example/deal/domain/model/Deal.kt | 2 ++ .../infrastructure/api/KakaoPaymentGateway.kt | 2 +- .../infrastructure/api/TossPaymentGateway.kt | 2 +- .../controller/GlobalExceptionHandller.kt | 22 --------------- .../application/service/TicketService.kt | 12 ++++---- .../org/example/ticket/domain/model/Ticket.kt | 28 ++++++------------- .../api/TicketApiClientResolver.kt | 14 ++++++++-- .../user/repository/UserJpaRepository.kt | 16 ----------- .../org/example/deal/domain/DealTest.kt | 6 ++-- .../org/example/ticket/domain/TicketTest.kt | 20 +++++++------ 12 files changed, 49 insertions(+), 84 deletions(-) delete mode 100644 src/main/kotlin/org/example/error/controller/GlobalExceptionHandller.kt delete mode 100644 src/main/kotlin/org/example/user/repository/UserJpaRepository.kt diff --git a/src/main/kotlin/org/example/deal/application/dto/DealEndDto.kt b/src/main/kotlin/org/example/deal/application/dto/DealEndDto.kt index 3d1d194..e48bf51 100644 --- a/src/main/kotlin/org/example/deal/application/dto/DealEndDto.kt +++ b/src/main/kotlin/org/example/deal/application/dto/DealEndDto.kt @@ -4,5 +4,5 @@ import org.example.deal.domain.enum.PaymentType data class DealEndDto( val barcode: String, - val payementType: PaymentType + val paymentType: PaymentType ) diff --git a/src/main/kotlin/org/example/deal/application/service/DealService.kt b/src/main/kotlin/org/example/deal/application/service/DealService.kt index 8b6a8ad..5da0941 100644 --- a/src/main/kotlin/org/example/deal/application/service/DealService.kt +++ b/src/main/kotlin/org/example/deal/application/service/DealService.kt @@ -1,5 +1,6 @@ package org.example.deal.application.service +import jakarta.transaction.Transactional import org.example.deal.application.dto.DealEndDto import org.example.deal.application.dto.DealResponseDto import org.example.deal.application.dto.DealStartDto @@ -15,7 +16,7 @@ class DealService( private val ticketRepository: TicketJpaRepository, private val paymentGatewayResolver: PaymentGatewayResolver ) { - + @Transactional fun dealStart(dealStartDto: DealStartDto): DealResponseDto { val ticket = requireNotNull(ticketRepository.findByBarcode(dealStartDto.barcode)) { "존재하지 않는 티켓입니다." } val sellingPrice = requireNotNull(ticket.sellingPrice) { "판매가가 설정되지 않았습니다." } @@ -39,7 +40,7 @@ class DealService( dealStatus = savedDeal.getDealStatus() ) } - + @Transactional fun dealEnd(dealEndDto: DealEndDto): DealResponseDto { val deal = requireNotNull(dealRepository.findByBarcode(dealEndDto.barcode)) { "존재하지 않는 거래입니다." } val ticket = requireNotNull(ticketRepository.findByBarcode(dealEndDto.barcode)) { "존재하지 않는 티켓입니다." } @@ -51,7 +52,7 @@ class DealService( dealRepository.save(deal) throw IllegalArgumentException("10분 이내 입금이 되지않아 결제가 취소되었습니다.") } - val paymentGateway = paymentGatewayResolver.resolve(dealEndDto.payementType) + val paymentGateway = paymentGatewayResolver.resolve(dealEndDto.paymentType) paymentGateway.pay(deal.buyerName, deal.sellerName, deal.sellingPrice) deal.dealComplete() ticket.ticketSold() diff --git a/src/main/kotlin/org/example/deal/domain/model/Deal.kt b/src/main/kotlin/org/example/deal/domain/model/Deal.kt index 0f8f336..98ef8f6 100644 --- a/src/main/kotlin/org/example/deal/domain/model/Deal.kt +++ b/src/main/kotlin/org/example/deal/domain/model/Deal.kt @@ -19,6 +19,8 @@ class Deal( val reservedDateTime: LocalDateTime = LocalDateTime.now(), private var dealStatus: DealStatus = DealStatus.RESERVED ) { + + companion object {} init { diff --git a/src/main/kotlin/org/example/deal/infrastructure/api/KakaoPaymentGateway.kt b/src/main/kotlin/org/example/deal/infrastructure/api/KakaoPaymentGateway.kt index 51b7e25..81f14e1 100644 --- a/src/main/kotlin/org/example/deal/infrastructure/api/KakaoPaymentGateway.kt +++ b/src/main/kotlin/org/example/deal/infrastructure/api/KakaoPaymentGateway.kt @@ -2,7 +2,7 @@ package org.example.deal.infrastructure.api import org.example.deal.domain.enum.PaymentType import org.springframework.stereotype.Component -import org.example.user.repository.UserJpaRepository +import org.example.user.infrastructure.repository.UserJpaRepository import java.math.BigDecimal diff --git a/src/main/kotlin/org/example/deal/infrastructure/api/TossPaymentGateway.kt b/src/main/kotlin/org/example/deal/infrastructure/api/TossPaymentGateway.kt index 8f43b1f..636a24b 100644 --- a/src/main/kotlin/org/example/deal/infrastructure/api/TossPaymentGateway.kt +++ b/src/main/kotlin/org/example/deal/infrastructure/api/TossPaymentGateway.kt @@ -1,7 +1,7 @@ package org.example.deal.infrastructure.api import org.example.deal.domain.enum.PaymentType -import org.example.user.repository.UserJpaRepository +import org.example.user.infrastructure.repository.UserJpaRepository import org.springframework.stereotype.Component import java.math.BigDecimal diff --git a/src/main/kotlin/org/example/error/controller/GlobalExceptionHandller.kt b/src/main/kotlin/org/example/error/controller/GlobalExceptionHandller.kt deleted file mode 100644 index 825056a..0000000 --- a/src/main/kotlin/org/example/error/controller/GlobalExceptionHandller.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.example.error.controller - -import org.springframework.http.HttpStatus -import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.ExceptionHandler -import org.springframework.web.bind.annotation.RestControllerAdvice - -@RestControllerAdvice -class GlobalExceptionHandler { - - @ExceptionHandler(IllegalArgumentException::class) - fun handleIllegalArgument(e: IllegalArgumentException): ResponseEntity { - return ResponseEntity - .status(HttpStatus.BAD_REQUEST) - .body( - ErrorResponse( - message = e.message ?: "잘못된 요청입니다.", - status = 400 - ) - ) - } -} \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/application/service/TicketService.kt b/src/main/kotlin/org/example/ticket/application/service/TicketService.kt index e16d332..a799d54 100644 --- a/src/main/kotlin/org/example/ticket/application/service/TicketService.kt +++ b/src/main/kotlin/org/example/ticket/application/service/TicketService.kt @@ -1,5 +1,6 @@ package org.example.ticket.application.service +import jakarta.transaction.Transactional import org.example.ticket.application.dto.TicketCreationDto import org.example.ticket.application.dto.TicketResponseDto import org.example.ticket.application.dto.TicketSellingPriceOfferDto @@ -12,6 +13,7 @@ class TicketService( private val ticketApiClientResolver: TicketApiClientResolver, private val ticketRepository: TicketJpaRepository ) { + @Transactional fun createTicket(ticketCreationDto: TicketCreationDto): TicketResponseDto { val apiClient = ticketApiClientResolver.resolve(ticketCreationDto.barcode) val ticketResponseDto = apiClient.getTicket(ticketCreationDto.barcode) @@ -28,10 +30,10 @@ class TicketService( originalPrice = ticket.originalPrice, sellingPrice = ticket.sellingPrice, expirationDate = ticket.expirationDateTime, - ticketStatus = ticket.getTicketStatus(), + ticketStatus = ticket.ticketStatus, ) } - + @Transactional fun applySellerOfferPrice(ticketSellingPriceOfferDto: TicketSellingPriceOfferDto): TicketResponseDto { val ticket = requireNotNull(ticketRepository.findByBarcode(ticketSellingPriceOfferDto.barcode)){ "해당 티켓 정보가 존재하지 않습니다." @@ -47,10 +49,10 @@ class TicketService( originalPrice = ticket.originalPrice, sellingPrice = ticket.sellingPrice, expirationDate = ticket.expirationDateTime, - ticketStatus = ticket.getTicketStatus(), + ticketStatus = ticket.ticketStatus, ) } - + @Transactional fun reSaleTicket(ticketSellingPriceOfferDto: TicketSellingPriceOfferDto): TicketResponseDto { val ticket = requireNotNull(ticketRepository.findByBarcode(ticketSellingPriceOfferDto.barcode)){ "해당 티켓 정보가 존재하지 않습니다." @@ -66,7 +68,7 @@ class TicketService( originalPrice = ticket.originalPrice, sellingPrice = ticket.sellingPrice, expirationDate = ticket.expirationDateTime, - ticketStatus = ticket.getTicketStatus(), + ticketStatus = ticket.ticketStatus, ) } } \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt index dbc00cb..8688692 100644 --- a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt +++ b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt @@ -17,10 +17,13 @@ class Ticket( val expirationDateTime: LocalDateTime, val originalPrice: BigDecimal, // BigDecimal => 10진수로 저장(소수점 계산을 위해) val sellerName: String, - var sellingPrice: BigDecimal? = null, - private var ticketStatus: TicketStatus = TicketStatus.ON_SALE, val ticketType: TicketType ) { + var sellingPrice: BigDecimal? = null + private set + var ticketStatus: TicketStatus = TicketStatus.ON_SALE + private set + companion object { private val HALF = BigDecimal("0.5") @@ -28,37 +31,25 @@ class Ticket( require(barcode.length == 8) { "바코드는 8자리여야 합니다." } } - fun ticketTypeCheck(barcode: String): TicketType { - return when { - barcode.all { it.isLetter() } -> TicketType.MELON - barcode.all { it.isDigit() } -> TicketType.NOL - else -> TicketType.MOL - } - - } } init { ticketBarcodeCheck(barcode) - ticketTimeCheck(expirationDateTime) + ticketTimeCheck() } - fun getTicketDeadLine(): LocalDateTime { + private fun getTicketDeadLine(): LocalDateTime { return expirationDateTime.minusHours(1) } - fun ticketTimeCheck(performanceDateTime: LocalDateTime) { + private fun ticketTimeCheck() { val now = LocalDateTime.now() - val deadLine: LocalDateTime = getTicketDeadLine(); + val deadLine = getTicketDeadLine() require(now.isBefore(deadLine)) { "공연 시작 1시간 전까지만 등록할 수 있습니다." } } - fun getTicketStatus(): TicketStatus { - return ticketStatus - } - fun applySellerOfferPrice(offerSellerName: String, offerPrice: BigDecimal) { require(sellerName == offerSellerName) { "티켓에 등록된 판매자가 아닙니다." } require(offerPrice >= BigDecimal.ZERO) { "판매가격에 음수는 입력할 수 없습니다." } @@ -101,7 +92,6 @@ class Ticket( require(now.isAfter(deadLine)) { "만료되지 않은 티켓입니다." } ticketStatus = TicketStatus.EXPIRED } - } diff --git a/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClientResolver.kt b/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClientResolver.kt index 6488c69..86c116c 100644 --- a/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClientResolver.kt +++ b/src/main/kotlin/org/example/ticket/infrastructure/api/TicketApiClientResolver.kt @@ -1,6 +1,6 @@ package org.example.ticket.infrastructure.api -import org.example.ticket.domain.model.Ticket +import org.example.ticket.domain.enum.TicketType import org.springframework.stereotype.Component @Component @@ -8,7 +8,15 @@ class TicketApiClientResolver( private val ticketApiClients: List ) { fun resolve(barcode: String): TicketApiClient { - val ticketType = Ticket.ticketTypeCheck(barcode) + val ticketType = resolveTicketType(barcode) return ticketApiClients.first { it.type() == ticketType } } -} \ No newline at end of file + + private fun resolveTicketType(barcode: String): TicketType { + return when { + barcode.all { it.isLetter() } -> TicketType.MELON + barcode.all { it.isDigit() } -> TicketType.NOL + else -> TicketType.MOL + } + } +} diff --git a/src/main/kotlin/org/example/user/repository/UserJpaRepository.kt b/src/main/kotlin/org/example/user/repository/UserJpaRepository.kt deleted file mode 100644 index d2afec0..0000000 --- a/src/main/kotlin/org/example/user/repository/UserJpaRepository.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.example.user.repository - -import org.example.user.domain.enum.UserRole -import org.example.user.domain.model.User -import java.math.BigDecimal - -object UserJpaRepository { - private val users = listOf( - User(id = 1, name = "판매자1", role = UserRole.SELLER, money = BigDecimal.ZERO), - User(id = 2, name = "판매자2", role = UserRole.SELLER, money = BigDecimal.ZERO), - User(id = 3, name = "구매자1", role = UserRole.BUYER, money = BigDecimal(10000)), - User(id = 4, name = "구매자2", role = UserRole.BUYER, money = BigDecimal(50000)), - ) - - fun findByName(name: String): User? = users.find { it.name == name } -} \ No newline at end of file diff --git a/src/test/kotlin/org/example/deal/domain/DealTest.kt b/src/test/kotlin/org/example/deal/domain/DealTest.kt index eca3d8f..d912c1c 100644 --- a/src/test/kotlin/org/example/deal/domain/DealTest.kt +++ b/src/test/kotlin/org/example/deal/domain/DealTest.kt @@ -5,8 +5,6 @@ import org.example.deal.domain.enum.DealStatus import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.assertj.core.api.Assertions.assertThat -import org.example.ticket.domain.enum.TicketType -import org.example.ticket.domain.model.Ticket import java.math.BigDecimal import java.time.LocalDateTime @@ -43,10 +41,10 @@ class DealTest { sellerName = "판매자1", buyerName = "구매자1", sellingPrice = BigDecimal(10000), - dealStatus = DealStatus.COMPLETED ) + deal.dealComplete() // RESERVED → COMPLETED assertThrows { - deal.dealComplete() + deal.dealComplete() // COMPLETED → 💥 예외 } } diff --git a/src/test/kotlin/org/example/ticket/domain/TicketTest.kt b/src/test/kotlin/org/example/ticket/domain/TicketTest.kt index 2c3ba7e..eca61aa 100644 --- a/src/test/kotlin/org/example/ticket/domain/TicketTest.kt +++ b/src/test/kotlin/org/example/ticket/domain/TicketTest.kt @@ -16,7 +16,7 @@ class TicketTest { id = 1L, barcode = "ABCDEFGH", sellerName = "Seller A", - expirationDateTime = LocalDateTime.now().plusHours(1), + expirationDateTime = LocalDateTime.now().plusHours(2), originalPrice = BigDecimal.TEN, ticketType = TicketType.MELON ) @@ -60,15 +60,17 @@ class TicketTest { @Test fun `양도 가격은 정가를 초과 할 수 없다`() { + val ticket = Ticket( + id = 1L, + barcode = "ABCDEFGH", + sellerName = "Seller A", + expirationDateTime = LocalDateTime.now().plusDays(1), + originalPrice = BigDecimal.TEN, + ticketType = TicketType.MELON + ) + assertThrows { - Ticket( - id = 1L, - barcode = "ABCDEFGH", - sellerName = "Seller A", - expirationDateTime = LocalDateTime.now().plusDays(1), - originalPrice = BigDecimal.TEN, - ticketType = TicketType.MELON - ) + ticket.applySellerOfferPrice("Seller A", BigDecimal(20)) } } } From 48df32240f60f415ba15420a16756380f5ae9fd8 Mon Sep 17 00:00:00 2001 From: songhansol Date: Mon, 9 Mar 2026 18:24:42 +0900 Subject: [PATCH 19/22] =?UTF-8?q?=EC=98=A4=ED=83=80=EC=88=98=EC=A0=95=20ge?= =?UTF-8?q?tter=20=EB=8C=80=EC=8B=A0=20private=20set=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=20=ED=8B=B0=EC=BC=93=ED=83=80=EC=9E=85=EC=B2=B4=ED=81=AC=20mod?= =?UTF-8?q?el=EC=97=90=EC=84=9C=20ApiClientResolver=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/GlobalExceptionHandler.kt | 19 +++++++++++++++++++ .../repository/UserJpaRepository.kt | 16 ++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 src/main/kotlin/org/example/error/controller/GlobalExceptionHandler.kt create mode 100644 src/main/kotlin/org/example/user/infrastructure/repository/UserJpaRepository.kt diff --git a/src/main/kotlin/org/example/error/controller/GlobalExceptionHandler.kt b/src/main/kotlin/org/example/error/controller/GlobalExceptionHandler.kt new file mode 100644 index 0000000..d0a23b9 --- /dev/null +++ b/src/main/kotlin/org/example/error/controller/GlobalExceptionHandler.kt @@ -0,0 +1,19 @@ +package org.example.error.controller + +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice + +@RestControllerAdvice +class GlobalExceptionHandler { + + @ExceptionHandler(IllegalArgumentException::class) + fun handleIllegalArgument(e: IllegalArgumentException): ResponseEntity { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body( + ErrorResponse( + message = e.message ?: "잘못된 요청입니다.", status = 400 + ) + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/example/user/infrastructure/repository/UserJpaRepository.kt b/src/main/kotlin/org/example/user/infrastructure/repository/UserJpaRepository.kt new file mode 100644 index 0000000..8327d1a --- /dev/null +++ b/src/main/kotlin/org/example/user/infrastructure/repository/UserJpaRepository.kt @@ -0,0 +1,16 @@ +package org.example.user.infrastructure.repository + +import org.example.user.domain.enum.UserRole +import org.example.user.domain.model.User +import java.math.BigDecimal + +object UserJpaRepository { + private val users = listOf( + User(id = 1, name = "판매자1", role = UserRole.SELLER, money = BigDecimal.ZERO), + User(id = 2, name = "판매자2", role = UserRole.SELLER, money = BigDecimal.ZERO), + User(id = 3, name = "구매자1", role = UserRole.BUYER, money = BigDecimal(10000)), + User(id = 4, name = "구매자2", role = UserRole.BUYER, money = BigDecimal(50000)), + ) + + fun findByName(name: String): User? = users.find { it.name == name } +} \ No newline at end of file From 76e1576b8ee17d95e04c1d1250c072c267566307 Mon Sep 17 00:00:00 2001 From: songhansol Date: Mon, 9 Mar 2026 18:25:12 +0900 Subject: [PATCH 20/22] 1 --- .../example/user/infrastructure/repository/UserJpaRepository.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/org/example/user/infrastructure/repository/UserJpaRepository.kt b/src/main/kotlin/org/example/user/infrastructure/repository/UserJpaRepository.kt index 8327d1a..f25dbe5 100644 --- a/src/main/kotlin/org/example/user/infrastructure/repository/UserJpaRepository.kt +++ b/src/main/kotlin/org/example/user/infrastructure/repository/UserJpaRepository.kt @@ -4,6 +4,7 @@ import org.example.user.domain.enum.UserRole import org.example.user.domain.model.User import java.math.BigDecimal + object UserJpaRepository { private val users = listOf( User(id = 1, name = "판매자1", role = UserRole.SELLER, money = BigDecimal.ZERO), From 66b6f00a3578d30abb56956168b35eaa889c80b7 Mon Sep 17 00:00:00 2001 From: songhansol Date: Tue, 10 Mar 2026 18:43:44 +0900 Subject: [PATCH 21/22] =?UTF-8?q?refactor(ticket):=20=EB=B0=94=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B2=B4=ED=81=AC=20=EB=A1=9C=EC=A7=81=EC=9D=84=20?= =?UTF-8?q?init=20=EB=B8=94=EB=A1=9D=EC=9C=BC=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20refactor(ticket):=20HALF=20=EC=83=81=EC=88=98?= =?UTF-8?q?=EB=A5=BC=20PERFORMANCE=5FDAY=5FSALE=5FPOLICY=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20refactor(ticket):=20=EB=B0=94=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EA=B8=B8=EC=9D=B4=20=EB=A7=A4=EC=A7=81=EB=84=98=EB=B2=84?= =?UTF-8?q?=EB=A5=BC=20BARCODE=5FLENGTH=20=EC=83=81=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20refactor(ticket):=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20reSale=20=ED=95=A8=EC=88=98=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20feat(ticket):=20applySellerOfferPrice=EC=97=90=20?= =?UTF-8?q?=EB=A7=8C=EB=A3=8C=20=ED=8B=B0=EC=BC=93=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EB=A7=8C=EB=A3=8C=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?refactor(ticket):=20@Transactional=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EA=B0=90=EC=A7=80=EB=A5=BC=20=ED=99=9C=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20save()=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20refactor(api):=20API=20=EC=97=94=EB=93=9C?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=EB=A5=BC=20restful=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?refactor(deal):=20getDealStatus=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=ED=9B=84=20private=20set=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=8C=80=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deal/application/dto/DealEndDto.kt | 1 - .../deal/application/service/DealService.kt | 32 ++++++++++--------- .../org/example/deal/domain/model/Deal.kt | 9 +++--- .../presentation/controller/DealController.kt | 15 ++++++--- .../dto/TicketSellingPriceOfferDto.kt | 1 - .../application/service/TicketService.kt | 25 ++------------- .../org/example/ticket/domain/model/Ticket.kt | 20 ++++-------- .../controller/TicketController.kt | 17 +++++----- 8 files changed, 49 insertions(+), 71 deletions(-) diff --git a/src/main/kotlin/org/example/deal/application/dto/DealEndDto.kt b/src/main/kotlin/org/example/deal/application/dto/DealEndDto.kt index e48bf51..da02873 100644 --- a/src/main/kotlin/org/example/deal/application/dto/DealEndDto.kt +++ b/src/main/kotlin/org/example/deal/application/dto/DealEndDto.kt @@ -3,6 +3,5 @@ package org.example.deal.application.dto import org.example.deal.domain.enum.PaymentType data class DealEndDto( - val barcode: String, val paymentType: PaymentType ) diff --git a/src/main/kotlin/org/example/deal/application/service/DealService.kt b/src/main/kotlin/org/example/deal/application/service/DealService.kt index 5da0941..dd3d40c 100644 --- a/src/main/kotlin/org/example/deal/application/service/DealService.kt +++ b/src/main/kotlin/org/example/deal/application/service/DealService.kt @@ -29,7 +29,6 @@ class DealService( ) ticket.ticketReserve() - ticketRepository.save(ticket) val savedDeal = dealRepository.save(deal) return DealResponseDto( barcode = savedDeal.barcode, @@ -41,30 +40,33 @@ class DealService( ) } @Transactional - fun dealEnd(dealEndDto: DealEndDto): DealResponseDto { - val deal = requireNotNull(dealRepository.findByBarcode(dealEndDto.barcode)) { "존재하지 않는 거래입니다." } - val ticket = requireNotNull(ticketRepository.findByBarcode(dealEndDto.barcode)) { "존재하지 않는 티켓입니다." } + fun dealEnd(barcode:String, dealEndDto: DealEndDto): DealResponseDto { + val deal = requireNotNull(dealRepository.findByBarcode(barcode)) { "존재하지 않는 거래입니다." } + val ticket = requireNotNull(ticketRepository.findByBarcode(barcode)) { "존재하지 않는 티켓입니다." } val dealExpiredCheck = deal.reservedTimeExpiredCheck() if (dealExpiredCheck) { deal.dealCancel() ticket.ticketOnSale() - ticketRepository.save(ticket) - dealRepository.save(deal) - throw IllegalArgumentException("10분 이내 입금이 되지않아 결제가 취소되었습니다.") + return DealResponseDto( + barcode = deal.barcode, + sellerName = deal.sellerName, + buyerName = deal.buyerName, + sellingPrice = deal.sellingPrice, + reservedDateTime = deal.reservedDateTime, + dealStatus = deal.dealStatus + ) } val paymentGateway = paymentGatewayResolver.resolve(dealEndDto.paymentType) paymentGateway.pay(deal.buyerName, deal.sellerName, deal.sellingPrice) deal.dealComplete() ticket.ticketSold() - ticketRepository.save(ticket) - val savedDeal = dealRepository.save(deal) return DealResponseDto( - barcode = savedDeal.barcode, - sellerName = savedDeal.sellerName, - buyerName = savedDeal.buyerName, - sellingPrice = savedDeal.sellingPrice, - reservedDateTime = savedDeal.reservedDateTime, - dealStatus = savedDeal.getDealStatus() + barcode = deal.barcode, + sellerName = deal.sellerName, + buyerName = deal.buyerName, + sellingPrice = deal.sellingPrice, + reservedDateTime = deal.reservedDateTime, + dealStatus = deal.getDealStatus() ) } diff --git a/src/main/kotlin/org/example/deal/domain/model/Deal.kt b/src/main/kotlin/org/example/deal/domain/model/Deal.kt index 98ef8f6..261dd1e 100644 --- a/src/main/kotlin/org/example/deal/domain/model/Deal.kt +++ b/src/main/kotlin/org/example/deal/domain/model/Deal.kt @@ -15,10 +15,12 @@ class Deal( val barcode: String, val sellerName: String, val buyerName: String, - val sellingPrice: BigDecimal, val reservedDateTime: LocalDateTime = LocalDateTime.now(), - private var dealStatus: DealStatus = DealStatus.RESERVED + val sellingPrice: BigDecimal, ) { + var dealStatus: DealStatus = DealStatus.RESERVED + private set + companion object {} @@ -42,7 +44,4 @@ class Deal( return dealStatus == DealStatus.RESERVED && reservedDateTime.plusMinutes(10).isBefore(LocalDateTime.now()) } - fun getDealStatus(): DealStatus { - return dealStatus - } } diff --git a/src/main/kotlin/org/example/deal/presentation/controller/DealController.kt b/src/main/kotlin/org/example/deal/presentation/controller/DealController.kt index 07d7394..2f35686 100644 --- a/src/main/kotlin/org/example/deal/presentation/controller/DealController.kt +++ b/src/main/kotlin/org/example/deal/presentation/controller/DealController.kt @@ -4,23 +4,28 @@ import org.example.deal.application.dto.DealEndDto import org.example.deal.application.dto.DealResponseDto import org.example.deal.application.dto.DealStartDto import org.example.deal.application.service.DealService +import org.springframework.web.bind.annotation.PatchMapping +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 @RestController -@RequestMapping(value = ["/deal"]) +@RequestMapping(value = ["/deals"]) class DealController( private val dealService: DealService ) { - @PostMapping("/start") + @PostMapping("") fun dealStart(@RequestBody dealStartDto: DealStartDto): DealResponseDto { return dealService.dealStart(dealStartDto) } - @PostMapping("/end") - fun dealEnd(@RequestBody dealEndDto: DealEndDto): DealResponseDto { - return dealService.dealEnd(dealEndDto) + @PatchMapping("/{barcode}") + fun dealEnd( + @PathVariable("barcode") barcode: String, + @RequestBody dealEndDto: DealEndDto + ): DealResponseDto { + return dealService.dealEnd(barcode, dealEndDto) } } \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/application/dto/TicketSellingPriceOfferDto.kt b/src/main/kotlin/org/example/ticket/application/dto/TicketSellingPriceOfferDto.kt index 98339c9..e8e83a8 100644 --- a/src/main/kotlin/org/example/ticket/application/dto/TicketSellingPriceOfferDto.kt +++ b/src/main/kotlin/org/example/ticket/application/dto/TicketSellingPriceOfferDto.kt @@ -4,6 +4,5 @@ import java.math.BigDecimal data class TicketSellingPriceOfferDto( val sellerName: String, - val barcode: String, val sellingPrice: BigDecimal, ) \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/application/service/TicketService.kt b/src/main/kotlin/org/example/ticket/application/service/TicketService.kt index a799d54..413ada1 100644 --- a/src/main/kotlin/org/example/ticket/application/service/TicketService.kt +++ b/src/main/kotlin/org/example/ticket/application/service/TicketService.kt @@ -34,34 +34,14 @@ class TicketService( ) } @Transactional - fun applySellerOfferPrice(ticketSellingPriceOfferDto: TicketSellingPriceOfferDto): TicketResponseDto { - val ticket = requireNotNull(ticketRepository.findByBarcode(ticketSellingPriceOfferDto.barcode)){ + fun applySellerOfferPrice(barcode: String, ticketSellingPriceOfferDto: TicketSellingPriceOfferDto): TicketResponseDto { + val ticket = requireNotNull(ticketRepository.findByBarcode(barcode)){ "해당 티켓 정보가 존재하지 않습니다." } ticket.applySellerOfferPrice( ticketSellingPriceOfferDto.sellerName, ticketSellingPriceOfferDto.sellingPrice ) - ticketRepository.save(ticket) - return TicketResponseDto( - barcode = ticket.barcode, - seller = ticket.sellerName, - originalPrice = ticket.originalPrice, - sellingPrice = ticket.sellingPrice, - expirationDate = ticket.expirationDateTime, - ticketStatus = ticket.ticketStatus, - ) - } - @Transactional - fun reSaleTicket(ticketSellingPriceOfferDto: TicketSellingPriceOfferDto): TicketResponseDto { - val ticket = requireNotNull(ticketRepository.findByBarcode(ticketSellingPriceOfferDto.barcode)){ - "해당 티켓 정보가 존재하지 않습니다." - } - ticket.reSale( - ticketSellingPriceOfferDto.sellerName, - ticketSellingPriceOfferDto.sellingPrice - ) - ticketRepository.save(ticket) return TicketResponseDto( barcode = ticket.barcode, seller = ticket.sellerName, @@ -71,4 +51,5 @@ class TicketService( ticketStatus = ticket.ticketStatus, ) } + } \ No newline at end of file diff --git a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt index 8688692..230fe6d 100644 --- a/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt +++ b/src/main/kotlin/org/example/ticket/domain/model/Ticket.kt @@ -25,16 +25,13 @@ class Ticket( private set companion object { - private val HALF = BigDecimal("0.5") - - fun ticketBarcodeCheck(barcode: String) { - require(barcode.length == 8) { "바코드는 8자리여야 합니다." } - } + private val BARCODE_LENGTH = 8; + private val PERFORMANCE_DAY_SALE_POLICY = BigDecimal("0.5") } init { - ticketBarcodeCheck(barcode) + require(barcode.length == BARCODE_LENGTH) { "바코드는 8자리여야 합니다." } ticketTimeCheck() } @@ -54,21 +51,18 @@ class Ticket( require(sellerName == offerSellerName) { "티켓에 등록된 판매자가 아닙니다." } require(offerPrice >= BigDecimal.ZERO) { "판매가격에 음수는 입력할 수 없습니다." } require(offerPrice <= originalPrice) { "티켓의 가격은 정가를 초과할 수 없습니다." } + if (ticketStatus != TicketStatus.SOLD && LocalDateTime.now().isAfter(getTicketDeadLine())) { + ticketExpired() + } require(ticketStatus == TicketStatus.ON_SALE) { "판매중인 제품만 가격을 수정할 수 있습니다." } val isPerformanceDateTime = LocalDateTime.now().toLocalDate() == expirationDateTime.toLocalDate() if (isPerformanceDateTime) { - val maxAllowedPrice = originalPrice.multiply(HALF) + val maxAllowedPrice = originalPrice.multiply(PERFORMANCE_DAY_SALE_POLICY) require(offerPrice <= maxAllowedPrice) { "공연 당일에는 가격을 정가의 50% 이하로만 설정할 수 있습니다." } } - sellingPrice = offerPrice } - fun reSale(offerSellerName: String, offerPrice: BigDecimal) { - require(ticketStatus == TicketStatus.SOLD) { "구매한 티켓만 판매할 수 있습니다." } - ticketStatus = TicketStatus.ON_SALE - applySellerOfferPrice(offerSellerName, offerPrice) - } fun ticketOnSale() { require(ticketStatus == TicketStatus.RESERVED) { "예약중인 티켓만 판매중으로 되돌릴 수 있습니다." } diff --git a/src/main/kotlin/org/example/ticket/presentation/controller/TicketController.kt b/src/main/kotlin/org/example/ticket/presentation/controller/TicketController.kt index c245c70..c1d3104 100644 --- a/src/main/kotlin/org/example/ticket/presentation/controller/TicketController.kt +++ b/src/main/kotlin/org/example/ticket/presentation/controller/TicketController.kt @@ -5,6 +5,7 @@ import org.example.ticket.application.dto.TicketResponseDto import org.example.ticket.application.dto.TicketSellingPriceOfferDto import org.example.ticket.application.service.TicketService import org.springframework.web.bind.annotation.PatchMapping +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 @@ -15,18 +16,16 @@ import org.springframework.web.bind.annotation.RestController class TicketController( private val ticketService: TicketService ) { - @PostMapping("/create") + @PostMapping("") fun createTicket(@RequestBody requestDto: TicketCreationDto): TicketResponseDto { return ticketService.createTicket(requestDto) } - @PatchMapping("/price") - fun applySellerOfferPrice(@RequestBody dto: TicketSellingPriceOfferDto): TicketResponseDto { - return ticketService.applySellerOfferPrice(dto) - } - - @PostMapping("/resale") - fun reSaleTicket(@RequestBody dto: TicketSellingPriceOfferDto): TicketResponseDto { - return ticketService.reSaleTicket(dto) + @PatchMapping("/{barcode}/price") + fun applySellerOfferPrice( + @PathVariable("barcode") barcode: String, + @RequestBody dto: TicketSellingPriceOfferDto + ): TicketResponseDto { + return ticketService.applySellerOfferPrice(barcode, dto) } } \ No newline at end of file From badd2527d092191e9037aab690e06093fe083d2a Mon Sep 17 00:00:00 2001 From: songhansol Date: Thu, 12 Mar 2026 19:07:02 +0900 Subject: [PATCH 22/22] =?UTF-8?q?refactor(deal):=20getDealStatus=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=82=AD=EC=A0=9C=20=ED=9B=84=20private?= =?UTF-8?q?=20set=EC=9C=BC=EB=A1=9C=20=EB=8C=80=EC=B2=B4=20feat(TicketTest?= =?UTF-8?q?):=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BC=80=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20+=20=EC=88=98=EC=A0=95=20feat(User):=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20feat(KakaoPa?= =?UTF-8?q?ymentGateWayTest):=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20refactor(UserRepository):=20=EC=98=A4=EB=B8=8C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=EC=97=90=EC=84=9C=20@Repository=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=EB=A1=9C=20=EB=B3=80=EA=B2=BD(=EB=A0=88?= =?UTF-8?q?=ED=8D=BC=EC=A7=80=ED=86=A0=EB=A6=AC=20jpa=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC=20=ED=86=B5=EC=9D=BC,=20DI=EC=A0=81=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deal/application/service/DealService.kt | 4 +- .../org/example/deal/domain/model/Deal.kt | 1 - .../infrastructure/api/KakaoPaymentGateway.kt | 8 +- .../infrastructure/api/TossPaymentGateway.kt | 8 +- .../org/example/user/domain/model/User.kt | 4 +- .../repository/UserJpaRepository.kt | 15 +-- .../org/example/deal/domain/DealTest.kt | 2 +- .../api/KakaoPaymentGatewayTest.kt | 48 +++++++ .../org/example/ticket/domain/TicketTest.kt | 117 ++++++++++++++---- .../org/example/user/domain/UserTest.kt | 62 ++++++++++ 10 files changed, 227 insertions(+), 42 deletions(-) create mode 100644 src/test/kotlin/org/example/deal/infrastructure/api/KakaoPaymentGatewayTest.kt create mode 100644 src/test/kotlin/org/example/user/domain/UserTest.kt diff --git a/src/main/kotlin/org/example/deal/application/service/DealService.kt b/src/main/kotlin/org/example/deal/application/service/DealService.kt index dd3d40c..d1eaa7b 100644 --- a/src/main/kotlin/org/example/deal/application/service/DealService.kt +++ b/src/main/kotlin/org/example/deal/application/service/DealService.kt @@ -36,7 +36,7 @@ class DealService( buyerName = savedDeal.buyerName, sellingPrice = savedDeal.sellingPrice, reservedDateTime = savedDeal.reservedDateTime, - dealStatus = savedDeal.getDealStatus() + dealStatus = savedDeal.dealStatus ) } @Transactional @@ -66,7 +66,7 @@ class DealService( buyerName = deal.buyerName, sellingPrice = deal.sellingPrice, reservedDateTime = deal.reservedDateTime, - dealStatus = deal.getDealStatus() + dealStatus = deal.dealStatus ) } diff --git a/src/main/kotlin/org/example/deal/domain/model/Deal.kt b/src/main/kotlin/org/example/deal/domain/model/Deal.kt index 261dd1e..a1d997d 100644 --- a/src/main/kotlin/org/example/deal/domain/model/Deal.kt +++ b/src/main/kotlin/org/example/deal/domain/model/Deal.kt @@ -22,7 +22,6 @@ class Deal( private set - companion object {} init { diff --git a/src/main/kotlin/org/example/deal/infrastructure/api/KakaoPaymentGateway.kt b/src/main/kotlin/org/example/deal/infrastructure/api/KakaoPaymentGateway.kt index 81f14e1..16dd447 100644 --- a/src/main/kotlin/org/example/deal/infrastructure/api/KakaoPaymentGateway.kt +++ b/src/main/kotlin/org/example/deal/infrastructure/api/KakaoPaymentGateway.kt @@ -7,10 +7,12 @@ import java.math.BigDecimal @Component -class KakaoPaymentGateway : PaymentGateway { +class KakaoPaymentGateway( + private val userJpaRepository: UserJpaRepository +) : PaymentGateway { override fun pay(buyerName: String, sellerName: String, price: BigDecimal) { - val buyer = requireNotNull(UserJpaRepository.findByName(buyerName)) { "구매자 정보를 찾을 수 없습니다." } - val seller = requireNotNull(UserJpaRepository.findByName(sellerName)) { "판매자 정보를 찾을 수 없습니다." } + val buyer = requireNotNull(userJpaRepository.findByName(buyerName)) { "구매자 정보를 찾을 수 없습니다." } + val seller = requireNotNull(userJpaRepository.findByName(sellerName)) { "판매자 정보를 찾을 수 없습니다." } buyer.withdraw(price) seller.deposit(price) } diff --git a/src/main/kotlin/org/example/deal/infrastructure/api/TossPaymentGateway.kt b/src/main/kotlin/org/example/deal/infrastructure/api/TossPaymentGateway.kt index 636a24b..2037425 100644 --- a/src/main/kotlin/org/example/deal/infrastructure/api/TossPaymentGateway.kt +++ b/src/main/kotlin/org/example/deal/infrastructure/api/TossPaymentGateway.kt @@ -6,10 +6,12 @@ import org.springframework.stereotype.Component import java.math.BigDecimal @Component -class TossPaymentGateway : PaymentGateway { +class TossPaymentGateway( + private val userJpaRepository: UserJpaRepository +) : PaymentGateway { override fun pay(buyerName: String, sellerName: String, price: BigDecimal) { - val buyer = requireNotNull(UserJpaRepository.findByName(buyerName)) { "구매자 정보를 찾을 수 없습니다." } - val seller = requireNotNull(UserJpaRepository.findByName(sellerName)) { "판매자 정보를 찾을 수 없습니다." } + val buyer = requireNotNull(userJpaRepository.findByName(buyerName)) { "구매자 정보를 찾을 수 없습니다." } + val seller = requireNotNull(userJpaRepository.findByName(sellerName)) { "판매자 정보를 찾을 수 없습니다." } buyer.withdraw(price) seller.deposit(price) } diff --git a/src/main/kotlin/org/example/user/domain/model/User.kt b/src/main/kotlin/org/example/user/domain/model/User.kt index 6a1073b..13604b2 100644 --- a/src/main/kotlin/org/example/user/domain/model/User.kt +++ b/src/main/kotlin/org/example/user/domain/model/User.kt @@ -8,10 +8,11 @@ class User( val id: Long? = null, val name: String, val role: UserRole, - var money: BigDecimal = BigDecimal.ZERO, + var money: BigDecimal = BigDecimal.ZERO ) { fun deposit(amount: BigDecimal) { require(amount > BigDecimal.ZERO) { "입금액은 0보다 커야 합니다." } + require(amount >= money) { "잔액이 부족합니다." } money = money.add(amount) } @@ -21,4 +22,5 @@ class User( money = money.subtract(amount) } + } \ No newline at end of file diff --git a/src/main/kotlin/org/example/user/infrastructure/repository/UserJpaRepository.kt b/src/main/kotlin/org/example/user/infrastructure/repository/UserJpaRepository.kt index f25dbe5..5c2d381 100644 --- a/src/main/kotlin/org/example/user/infrastructure/repository/UserJpaRepository.kt +++ b/src/main/kotlin/org/example/user/infrastructure/repository/UserJpaRepository.kt @@ -2,15 +2,16 @@ package org.example.user.infrastructure.repository import org.example.user.domain.enum.UserRole import org.example.user.domain.model.User +import org.springframework.stereotype.Repository import java.math.BigDecimal - -object UserJpaRepository { - private val users = listOf( - User(id = 1, name = "판매자1", role = UserRole.SELLER, money = BigDecimal.ZERO), - User(id = 2, name = "판매자2", role = UserRole.SELLER, money = BigDecimal.ZERO), - User(id = 3, name = "구매자1", role = UserRole.BUYER, money = BigDecimal(10000)), - User(id = 4, name = "구매자2", role = UserRole.BUYER, money = BigDecimal(50000)), +@Repository +class UserJpaRepository { + private val users = mutableListOf( + User(id = 1, name = "판매자1", role = UserRole.SELLER), + User(id = 2, name = "판매자2", role = UserRole.SELLER), + User(id = 3, name = "구매자1", role = UserRole.BUYER).apply { deposit(BigDecimal(10000)) }, + User(id = 4, name = "구매자2", role = UserRole.BUYER).apply { deposit(BigDecimal(50000)) }, ) fun findByName(name: String): User? = users.find { it.name == name } diff --git a/src/test/kotlin/org/example/deal/domain/DealTest.kt b/src/test/kotlin/org/example/deal/domain/DealTest.kt index d912c1c..15cc70f 100644 --- a/src/test/kotlin/org/example/deal/domain/DealTest.kt +++ b/src/test/kotlin/org/example/deal/domain/DealTest.kt @@ -30,7 +30,7 @@ class DealTest { buyerName = "구매자1", sellingPrice = BigDecimal(10000) ) - assertThat(deal.getDealStatus()).isEqualTo(DealStatus.RESERVED) + assertThat(deal.dealStatus).isEqualTo(DealStatus.RESERVED) } diff --git a/src/test/kotlin/org/example/deal/infrastructure/api/KakaoPaymentGatewayTest.kt b/src/test/kotlin/org/example/deal/infrastructure/api/KakaoPaymentGatewayTest.kt new file mode 100644 index 0000000..b577d94 --- /dev/null +++ b/src/test/kotlin/org/example/deal/infrastructure/api/KakaoPaymentGatewayTest.kt @@ -0,0 +1,48 @@ + +package org.example.deal.infrastructure.api + +import org.assertj.core.api.Assertions.assertThat +import org.example.user.domain.enum.UserRole +import org.example.user.domain.model.User +import org.example.user.infrastructure.repository.UserJpaRepository +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.junit.jupiter.MockitoExtension +import java.math.BigDecimal + +@ExtendWith(MockitoExtension::class) +class KakaoPaymentGatewayTest { + + @Mock + private lateinit var userJpaRepository: UserJpaRepository + + @InjectMocks + private lateinit var kakaoPaymentGateway: KakaoPaymentGateway + + @Test + fun `결제에 성공한다`() { + val buyer = User(name = "구매자1", role = UserRole.BUYER).apply { + deposit(BigDecimal(50000)) } + val seller = User(name = "판매자1", role = UserRole.SELLER) + + `when`(userJpaRepository.findByName("구매자1")).thenReturn(buyer) + `when`(userJpaRepository.findByName("판매자1")).thenReturn(seller) + + kakaoPaymentGateway.pay("구매자1", "판매자1", BigDecimal(10000)) + assertThat(buyer.money).isEqualTo(BigDecimal(40000)) + assert(seller.money == BigDecimal(10000)) + } + + @Test + fun `존재하지 않는 구매자일경우 구매할 수 없다`() { + // Mock은 기본적으로 null을 반환하므로 + // findByName("없는유저") → null → requireNotNull에서 예외 발생 + assertThrows { + kakaoPaymentGateway.pay("없는유저", "판매자1", BigDecimal(10000)) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/org/example/ticket/domain/TicketTest.kt b/src/test/kotlin/org/example/ticket/domain/TicketTest.kt index eca61aa..6e17926 100644 --- a/src/test/kotlin/org/example/ticket/domain/TicketTest.kt +++ b/src/test/kotlin/org/example/ticket/domain/TicketTest.kt @@ -2,75 +2,144 @@ package org.example.ticket.domain import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.SoftAssertions.assertSoftly +import org.example.ticket.domain.enum.TicketStatus import org.example.ticket.domain.enum.TicketType import org.example.ticket.domain.model.Ticket +import org.example.user.domain.enum.UserRole +import org.example.user.domain.model.User import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import java.math.BigDecimal import java.time.LocalDateTime class TicketTest { - @Test - fun `티켓_만들기`() { - val ticket = Ticket( - id = 1L, - barcode = "ABCDEFGH", - sellerName = "Seller A", - expirationDateTime = LocalDateTime.now().plusHours(2), - originalPrice = BigDecimal.TEN, - ticketType = TicketType.MELON + private fun createUser( + name: String = "테스트유저", + role: UserRole = UserRole.BUYER, + ): User = User(name = name, role = role) + + private fun createTicket( + barcode: String = "ABCDEFGH", + sellerName: String = "Seller A", + expirationDateTime: LocalDateTime = LocalDateTime.now().plusHours(2), + originalPrice: BigDecimal = BigDecimal.TEN, + ticketType: TicketType = TicketType.MELON + ): Ticket { + return Ticket( + barcode = barcode, + sellerName = sellerName, + expirationDateTime = expirationDateTime, + originalPrice = originalPrice, + ticketType = ticketType ) + } + @Test + fun `티켓_만들기`() { + val ticket = createTicket(); assertSoftly { assertThat(ticket.id).isEqualTo(1L) + assertThat(ticket.ticketStatus).isEqualTo(TicketStatus.ON_SALE) assertThat(ticket.expirationDateTime).isNotNull assertThat(ticket.originalPrice).isEqualTo(BigDecimal.TEN) } } + @Test + fun `바코드가 8자리가 아니면 에러`() { + assertThrows { + val ticket = createTicket(barcode= "ABC") + } + } + + @Test fun `티켓 유효기간이 등록 기준 시간보다 늦으면 예외이다`() { assertThrows { - Ticket( - barcode = "ABCDEFGH", - sellerName = "Seller A", - expirationDateTime = LocalDateTime.now().plusHours(1).minusSeconds(1), - originalPrice = BigDecimal.TEN, - ticketType = TicketType.MELON - ) + createTicket(expirationDateTime = LocalDateTime.now().plusHours(1).minusSeconds(1),) } } @Test fun `공연 당일에는 가격을 정가의 50% 이하로만 설정 할 수 있다`() { - val ticketPrice = BigDecimal(10000) + val ticket = createTicket(originalPrice = BigDecimal(10000)) + assertThrows { + ticket.applySellerOfferPrice("Seller A", BigDecimal(6000)) + } + } + + @Test + fun `양도 가격은 정가를 초과 할 수 없다`() { val ticket = Ticket( id = 1L, barcode = "ABCDEFGH", sellerName = "Seller A", - expirationDateTime = LocalDateTime.now().plusHours(2), - originalPrice = ticketPrice, + expirationDateTime = LocalDateTime.now().plusDays(1), + originalPrice = BigDecimal.TEN, ticketType = TicketType.MELON ) assertThrows { - ticket.applySellerOfferPrice("Seller A", BigDecimal(6000)) + ticket.applySellerOfferPrice("Seller A", BigDecimal(20)) } } - @Test - fun `양도 가격은 정가를 초과 할 수 없다`() { + fun `판매중인 티켓을 예약할 수 있다`() { val ticket = Ticket( id = 1L, barcode = "ABCDEFGH", sellerName = "Seller A", - expirationDateTime = LocalDateTime.now().plusDays(1), + expirationDateTime = LocalDateTime.now().plusHours(2), originalPrice = BigDecimal.TEN, ticketType = TicketType.MELON ) + ticket.ticketReserve() + assertThat(ticket.ticketStatus).isEqualTo(TicketStatus.RESERVED) + } + @Test + fun `예약중인 티켓을 판매중으로 되돌릴 수 있다`() { + val ticket = Ticket( + id = 1L, + barcode = "ABCDEFGH", + sellerName = "Seller A", + expirationDateTime = LocalDateTime.now().plusHours(2), + originalPrice = BigDecimal.TEN, + ticketType = TicketType.MELON + ) + ticket.ticketReserve() + ticket.ticketOnSale() + assertThat(ticket.ticketStatus).isEqualTo(TicketStatus.ON_SALE) + } + @Test + fun `예약중인 티켓만 판매완료할 수 있다`() { + val ticket = Ticket( + id = 1L, + barcode = "ABCDEFGH", + sellerName = "Seller A", + expirationDateTime = LocalDateTime.now().plusHours(2), + originalPrice = BigDecimal.TEN, + ticketType = TicketType.MELON, + ) + ticket.ticketReserve() + ticket.ticketSold() + assertThat(ticket.ticketStatus).isEqualTo(TicketStatus.SOLD) + } + @Test + fun `판매중 상태에서 바로 판매완료할 수 없다`() { + val ticket = Ticket( + id = 1L, + barcode = "ABCDEFGH", + sellerName = "Seller A", + expirationDateTime = LocalDateTime.now().plusHours(2), + originalPrice = BigDecimal.TEN, + ticketType = TicketType.MELON, + ) assertThrows { - ticket.applySellerOfferPrice("Seller A", BigDecimal(20)) + ticket.ticketSold() } } + + + } diff --git a/src/test/kotlin/org/example/user/domain/UserTest.kt b/src/test/kotlin/org/example/user/domain/UserTest.kt new file mode 100644 index 0000000..44fd784 --- /dev/null +++ b/src/test/kotlin/org/example/user/domain/UserTest.kt @@ -0,0 +1,62 @@ +package org.example.user.domain + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.SoftAssertions.assertSoftly +import org.example.user.domain.enum.UserRole +import org.example.user.domain.model.User +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.math.BigDecimal + +class UserTest { + private fun createUser( + name: String = "테스트유저", + role: UserRole = UserRole.BUYER, + money: BigDecimal = BigDecimal(0), + ): User = User(name = name, role = role) + + @Test + fun `입금에 성공한다`() { + val user = createUser() + user.deposit(BigDecimal(10000)) + assertThat(user.money).isEqualTo(BigDecimal(10000)) + } + + @Test + fun `출금에 성공한다`() { + val user = createUser(money = BigDecimal(10000)) + user.withdraw(BigDecimal(3000)) + assertThat(user.money).isEqualTo(BigDecimal(7000)) + } + @Test + fun `0원을 입금할 수 없다`(){ + val user = createUser(money = BigDecimal(10000)) + assertThrows { + user.deposit(BigDecimal.ZERO) + } + } + @Test + fun `0원을 출금할 수 없다`(){ + val user = createUser() + assertThrows { + user.withdraw(BigDecimal.ZERO) + } + } + @Test + fun `잔액이 부족하면 입금할 수 없다`() { + val user = createUser() + assertThrows { + user.deposit(BigDecimal(5000)) + } + } + @Test + fun `출금액이 부족하면 출금할 수 없다`() { + val user = createUser() + assertSoftly { + assertThrows { + user.withdraw(BigDecimal(1000)) + } + } + } + +} \ No newline at end of file