From 08b9920ee3d2a3318a2f841aacfea2296fbcbe36 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 2 Oct 2025 13:30:30 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20ScheduleEntity=EC=97=90=20\@LastMod?= =?UTF-8?q?ifiedBy=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EC=9D=B4=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20hold=20API?= =?UTF-8?q?=EB=8A=94=20Update=20=EC=BF=BC=EB=A6=AC=EB=A5=BC=20=EB=B0=94?= =?UTF-8?q?=EB=A1=9C=20=EC=93=B0=EB=8F=84=EB=A1=9D=20=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/business/ScheduleService.kt | 15 ++- .../persistence/ScheduleEntity.kt | 34 +------ .../persistence/ScheduleRepository.kt | 14 +++ .../roomescape/schedule/ScheduleApiTest.kt | 11 ++- .../schedule/ScheduleServiceTest.kt | 94 +++++++++++++++++++ 5 files changed, 128 insertions(+), 40 deletions(-) create mode 100644 service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleServiceTest.kt diff --git a/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleService.kt b/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleService.kt index 1bd6491c..0bb2d24a 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleService.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleService.kt @@ -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" } } // ======================================== diff --git a/service/src/main/kotlin/com/sangdol/roomescape/schedule/infrastructure/persistence/ScheduleEntity.kt b/service/src/main/kotlin/com/sangdol/roomescape/schedule/infrastructure/persistence/ScheduleEntity.kt index fde4b384..d1379c62 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/schedule/infrastructure/persistence/ScheduleEntity.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/schedule/infrastructure/persistence/ScheduleEntity.kt @@ -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() } + ) } } diff --git a/service/src/main/kotlin/com/sangdol/roomescape/schedule/infrastructure/persistence/ScheduleRepository.kt b/service/src/main/kotlin/com/sangdol/roomescape/schedule/infrastructure/persistence/ScheduleRepository.kt index abf9484a..c0d92c1a 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/schedule/infrastructure/persistence/ScheduleRepository.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/schedule/infrastructure/persistence/ScheduleRepository.kt @@ -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 { """ ) 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 } diff --git a/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleApiTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleApiTest.kt index bbc55575..c5d25372 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleApiTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleApiTest.kt @@ -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 + ) + } } } } diff --git a/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleServiceTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleServiceTest.kt new file mode 100644 index 00000000..51f47d75 --- /dev/null +++ b/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleServiceTest.kt @@ -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 + } +}