refactor: ScheduleEntity에 \@LastModifiedBy 추가 및 회원이 사용하는 hold API는 Update 쿼리를 바로 쓰도록 하여 업데이트 방지

This commit is contained in:
이상진 2025-10-02 13:30:30 +09:00
parent 86a2459d8b
commit 08b9920ee3
5 changed files with 128 additions and 40 deletions

View File

@ -2,15 +2,14 @@ package com.sangdol.roomescape.schedule.business
import ScheduleException import ScheduleException
import com.sangdol.common.persistence.IDGenerator 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.AuditingInfo
import com.sangdol.roomescape.common.types.Auditor 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.business.domain.ScheduleOverview
import com.sangdol.roomescape.schedule.exception.ScheduleErrorCode import com.sangdol.roomescape.schedule.exception.ScheduleErrorCode
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleEntity import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleEntity
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleEntityFactory import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleEntityFactory
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus
import com.sangdol.roomescape.schedule.web.* import com.sangdol.roomescape.schedule.web.*
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
@ -18,7 +17,6 @@ import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@ -67,14 +65,15 @@ class ScheduleService(
// ======================================== // ========================================
@Transactional @Transactional
fun holdSchedule(id: Long) { 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) { if (result == 0) {
schedule.hold() log.info { "[ScheduleService.holdSchedule] Holding된 일정 없음: id=${id}, count = " }
return throw ScheduleException(ScheduleErrorCode.SCHEDULE_NOT_AVAILABLE)
} }
throw ScheduleException(ScheduleErrorCode.SCHEDULE_NOT_AVAILABLE) log.info { "[ScheduleService.holdSchedule] 일정 Holding 완료: id=$id" }
} }
// ======================================== // ========================================

View File

@ -1,14 +1,9 @@
package com.sangdol.roomescape.schedule.infrastructure.persistence package com.sangdol.roomescape.schedule.infrastructure.persistence
import com.sangdol.common.persistence.PersistableBaseEntity import com.sangdol.common.persistence.AuditingBaseEntity
import com.sangdol.common.utils.MdcPrincipalIdUtil
import jakarta.persistence.* 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 org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
@Entity @Entity
@ -24,36 +19,13 @@ class ScheduleEntity(
@Enumerated(value = EnumType.STRING) @Enumerated(value = EnumType.STRING)
var status: ScheduleStatus, var status: ScheduleStatus,
) : PersistableBaseEntity(id) { ) : AuditingBaseEntity(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
fun modifyIfNotNull( fun modifyIfNotNull(
time: LocalTime?, time: LocalTime?,
status: ScheduleStatus? status: ScheduleStatus?
) { ) {
time?.let { this.time = it } time?.let { this.time = it }
status?.let { this.status = 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, storeId = storeId,
themeId = themeId, themeId = themeId,
status = ScheduleStatus.AVAILABLE status = ScheduleStatus.AVAILABLE
).apply { this.updateLastModifiedBy() } )
} }
} }

View File

@ -3,6 +3,7 @@ package com.sangdol.roomescape.schedule.infrastructure.persistence
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query import org.springframework.data.jpa.repository.Query
import com.sangdol.roomescape.schedule.business.domain.ScheduleOverview import com.sangdol.roomescape.schedule.business.domain.ScheduleOverview
import org.springframework.data.jpa.repository.Modifying
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalTime import java.time.LocalTime
@ -82,4 +83,17 @@ interface ScheduleRepository : JpaRepository<ScheduleEntity, Long> {
""" """
) )
fun findOverviewByIdOrNull(id: Long): ScheduleOverview? 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
} }

View File

@ -130,7 +130,7 @@ class ScheduleApiTest(
context("일정이 ${ScheduleStatus.AVAILABLE}이 아니면 실패한다.") { context("일정이 ${ScheduleStatus.AVAILABLE}이 아니면 실패한다.") {
(ScheduleStatus.entries - ScheduleStatus.AVAILABLE).forEach { (ScheduleStatus.entries - ScheduleStatus.AVAILABLE).forEach {
test("${it}") { test("$it") {
val schedule = dummyInitializer.createSchedule(status = it) val schedule = dummyInitializer.createSchedule(status = it)
runExceptionTest( 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
)
}
} }
} }
} }

View File

@ -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
}
}