generated from pricelees/issue-pr-template
[#52] 만료 예약 / 일정 스케쥴링 작업 추가 및 동시성 처리를 위한 일부 코드 수정 #53
@ -2,15 +2,14 @@ package com.sangdol.roomescape.schedule.business
|
||||
|
||||
import ScheduleException
|
||||
import com.sangdol.common.persistence.IDGenerator
|
||||
import com.sangdol.roomescape.admin.business.AdminService
|
||||
import com.sangdol.roomescape.common.types.AuditingInfo
|
||||
import com.sangdol.roomescape.common.types.Auditor
|
||||
import com.sangdol.roomescape.admin.business.AdminService
|
||||
import com.sangdol.roomescape.schedule.business.domain.ScheduleOverview
|
||||
import com.sangdol.roomescape.schedule.exception.ScheduleErrorCode
|
||||
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleEntity
|
||||
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleEntityFactory
|
||||
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository
|
||||
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus
|
||||
import com.sangdol.roomescape.schedule.web.*
|
||||
import io.github.oshai.kotlinlogging.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
@ -18,7 +17,6 @@ import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
@ -67,14 +65,15 @@ class ScheduleService(
|
||||
// ========================================
|
||||
@Transactional
|
||||
fun holdSchedule(id: Long) {
|
||||
val schedule: ScheduleEntity = findOrThrow(id)
|
||||
log.info { "[ScheduleService.holdSchedule] 일정 Holding 시작: id=$id" }
|
||||
val result: Int = scheduleRepository.holdAvailableScheduleById(id)
|
||||
|
||||
if (schedule.status == ScheduleStatus.AVAILABLE) {
|
||||
schedule.hold()
|
||||
return
|
||||
if (result == 0) {
|
||||
log.info { "[ScheduleService.holdSchedule] Holding된 일정 없음: id=${id}, count = " }
|
||||
throw ScheduleException(ScheduleErrorCode.SCHEDULE_NOT_AVAILABLE)
|
||||
}
|
||||
|
||||
throw ScheduleException(ScheduleErrorCode.SCHEDULE_NOT_AVAILABLE)
|
||||
log.info { "[ScheduleService.holdSchedule] 일정 Holding 완료: id=$id" }
|
||||
}
|
||||
|
||||
// ========================================
|
||||
|
||||
@ -1,14 +1,9 @@
|
||||
package com.sangdol.roomescape.schedule.infrastructure.persistence
|
||||
|
||||
import com.sangdol.common.persistence.PersistableBaseEntity
|
||||
import com.sangdol.common.utils.MdcPrincipalIdUtil
|
||||
import com.sangdol.common.persistence.AuditingBaseEntity
|
||||
import jakarta.persistence.*
|
||||
import org.springframework.data.annotation.CreatedBy
|
||||
import org.springframework.data.annotation.CreatedDate
|
||||
import org.springframework.data.annotation.LastModifiedDate
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
|
||||
@Entity
|
||||
@ -24,36 +19,13 @@ class ScheduleEntity(
|
||||
|
||||
@Enumerated(value = EnumType.STRING)
|
||||
var status: ScheduleStatus,
|
||||
) : PersistableBaseEntity(id) {
|
||||
@Column(updatable = false)
|
||||
@CreatedDate
|
||||
lateinit var createdAt: LocalDateTime
|
||||
|
||||
@Column(updatable = false)
|
||||
@CreatedBy
|
||||
var createdBy: Long = 0L
|
||||
|
||||
@Column
|
||||
@LastModifiedDate
|
||||
lateinit var updatedAt: LocalDateTime
|
||||
|
||||
var updatedBy: Long = 0L
|
||||
|
||||
) : AuditingBaseEntity(id) {
|
||||
fun modifyIfNotNull(
|
||||
time: LocalTime?,
|
||||
status: ScheduleStatus?
|
||||
) {
|
||||
time?.let { this.time = it }
|
||||
status?.let { this.status = it }
|
||||
updateLastModifiedBy()
|
||||
}
|
||||
|
||||
fun hold() {
|
||||
this.status = ScheduleStatus.HOLD
|
||||
}
|
||||
|
||||
fun updateLastModifiedBy() {
|
||||
MdcPrincipalIdUtil.extractAsLongOrNull()?.also { this.updatedBy = it }
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,7 +38,7 @@ object ScheduleEntityFactory {
|
||||
storeId = storeId,
|
||||
themeId = themeId,
|
||||
status = ScheduleStatus.AVAILABLE
|
||||
).apply { this.updateLastModifiedBy() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ package com.sangdol.roomescape.schedule.infrastructure.persistence
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import com.sangdol.roomescape.schedule.business.domain.ScheduleOverview
|
||||
import org.springframework.data.jpa.repository.Modifying
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
|
||||
@ -82,4 +83,17 @@ interface ScheduleRepository : JpaRepository<ScheduleEntity, Long> {
|
||||
"""
|
||||
)
|
||||
fun findOverviewByIdOrNull(id: Long): ScheduleOverview?
|
||||
|
||||
@Modifying
|
||||
@Query("""
|
||||
UPDATE
|
||||
ScheduleEntity s
|
||||
SET
|
||||
s.status = 'HOLD'
|
||||
WHERE
|
||||
s._id = :id
|
||||
AND
|
||||
s.status = 'AVAILABLE'
|
||||
""")
|
||||
fun holdAvailableScheduleById(id: Long): Int
|
||||
}
|
||||
|
||||
@ -130,7 +130,7 @@ class ScheduleApiTest(
|
||||
|
||||
context("일정이 ${ScheduleStatus.AVAILABLE}이 아니면 실패한다.") {
|
||||
(ScheduleStatus.entries - ScheduleStatus.AVAILABLE).forEach {
|
||||
test("${it}") {
|
||||
test("$it") {
|
||||
val schedule = dummyInitializer.createSchedule(status = it)
|
||||
|
||||
runExceptionTest(
|
||||
@ -142,6 +142,15 @@ class ScheduleApiTest(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("일정이 없으면 실패한다.") {
|
||||
runExceptionTest(
|
||||
token = testAuthUtil.defaultUserLogin().second,
|
||||
method = HttpMethod.POST,
|
||||
endpoint = "/schedules/${INVALID_PK}/hold",
|
||||
expectedErrorCode = ScheduleErrorCode.SCHEDULE_NOT_AVAILABLE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,94 @@
|
||||
package com.sangdol.roomescape.schedule
|
||||
|
||||
import com.sangdol.common.utils.MdcPrincipalIdUtil
|
||||
import com.sangdol.roomescape.schedule.business.ScheduleService
|
||||
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository
|
||||
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus
|
||||
import com.sangdol.roomescape.schedule.web.ScheduleCreateRequest
|
||||
import com.sangdol.roomescape.schedule.web.ScheduleUpdateRequest
|
||||
import com.sangdol.roomescape.supports.FunSpecSpringbootTest
|
||||
import com.sangdol.roomescape.supports.IDGenerator
|
||||
import com.sangdol.roomescape.supports.initialize
|
||||
import io.kotest.assertions.assertSoftly
|
||||
import io.kotest.matchers.nulls.shouldNotBeNull
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.shouldNotBe
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
|
||||
class ScheduleServiceTest(
|
||||
private val scheduleService: ScheduleService,
|
||||
private val scheduleRepository: ScheduleRepository
|
||||
) : FunSpecSpringbootTest() {
|
||||
init {
|
||||
afterTest {
|
||||
MdcPrincipalIdUtil.clear()
|
||||
}
|
||||
|
||||
test("관리자가 일정을 수정하면 updatedAt, updatedBy 컬럼이 변경된다. ") {
|
||||
val createdScheduleId = createSchedule()
|
||||
|
||||
initialize("updatedBy 변경 확인을 위한 MDC 재설정") {
|
||||
MdcPrincipalIdUtil.clear()
|
||||
IDGenerator.create().also {
|
||||
MdcPrincipalIdUtil.set(it.toString())
|
||||
}
|
||||
}
|
||||
|
||||
scheduleService.updateSchedule(
|
||||
createdScheduleId,
|
||||
ScheduleUpdateRequest(status = ScheduleStatus.RESERVED)
|
||||
)
|
||||
|
||||
assertSoftly(scheduleRepository.findByIdOrNull(createdScheduleId)) {
|
||||
this.shouldNotBeNull()
|
||||
this.updatedAt shouldNotBe this.createdAt
|
||||
this.updatedBy shouldNotBe this.createdBy
|
||||
}
|
||||
}
|
||||
|
||||
test("유저가 일정을 Holding 하는 경우에는 updatedAt, updatedBy 컬럼이 변경되지 않는다.") {
|
||||
val createdScheduleId: Long = createSchedule()
|
||||
|
||||
initialize("updatedBy 변경 확인을 위한 MDC 재설정") {
|
||||
MdcPrincipalIdUtil.clear()
|
||||
IDGenerator.create().also {
|
||||
MdcPrincipalIdUtil.set(it.toString())
|
||||
}
|
||||
}
|
||||
|
||||
scheduleService.holdSchedule(createdScheduleId)
|
||||
|
||||
assertSoftly(scheduleRepository.findByIdOrNull(createdScheduleId)) {
|
||||
this.shouldNotBeNull()
|
||||
this.updatedAt shouldBe this.createdAt
|
||||
this.updatedBy shouldBe this.createdBy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createSchedule(): Long {
|
||||
initialize("createdBy, updatedBy 설정을 위한 MDC 설정") {
|
||||
IDGenerator.create().also {
|
||||
MdcPrincipalIdUtil.set(it.toString())
|
||||
}
|
||||
}
|
||||
|
||||
val (storeId, themeId) = initialize("FK 제약조건 해소를 위한 매장, 테마 생성") {
|
||||
val store = dummyInitializer.createStore()
|
||||
val theme = dummyInitializer.createTheme()
|
||||
|
||||
store.id to theme.id
|
||||
}
|
||||
|
||||
return scheduleService.createSchedule(
|
||||
storeId = storeId,
|
||||
request = ScheduleCreateRequest(
|
||||
date = LocalDate.now().plusDays(1),
|
||||
time = LocalTime.now(),
|
||||
themeId = themeId
|
||||
)
|
||||
).id
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user