refactor: schedule 관련 로직 및 테스트에 한국 시간 반영

This commit is contained in:
이상진 2025-10-05 20:28:03 +09:00
parent 6c2e63e35e
commit 844c8b6007
4 changed files with 77 additions and 44 deletions

View File

@ -1,6 +1,8 @@
package com.sangdol.roomescape.schedule.business package com.sangdol.roomescape.schedule.business
import com.sangdol.common.persistence.IDGenerator import com.sangdol.common.persistence.IDGenerator
import com.sangdol.common.utils.KoreaDate
import com.sangdol.common.utils.KoreaTime
import com.sangdol.roomescape.admin.business.AdminService 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
@ -44,7 +46,9 @@ class ScheduleService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun getStoreScheduleByDate(storeId: Long, date: LocalDate): ScheduleWithThemeListResponse { fun getStoreScheduleByDate(storeId: Long, date: LocalDate): ScheduleWithThemeListResponse {
log.info { "[getStoreScheduleByDate] 매장 일정 조회: storeId=${storeId}, date=$date" } log.info { "[getStoreScheduleByDate] 매장 일정 조회: storeId=${storeId}, date=$date" }
val currentDate = LocalDate.now()
val currentDate: LocalDate = KoreaDate.today()
val currentTime: LocalTime = KoreaTime.now()
if (date.isBefore(currentDate)) { if (date.isBefore(currentDate)) {
log.warn { "[getStoreScheduleByDate] 이전 날짜 선택으로 인한 실패: date=${date}" } log.warn { "[getStoreScheduleByDate] 이전 날짜 선택으로 인한 실패: date=${date}" }
@ -53,7 +57,7 @@ class ScheduleService(
val schedules: List<ScheduleOverview> = val schedules: List<ScheduleOverview> =
scheduleRepository.findStoreSchedulesWithThemeByDate(storeId, date) scheduleRepository.findStoreSchedulesWithThemeByDate(storeId, date)
.filter { it.date.isAfter(date) || (it.date.isEqual(date) && it.time.isAfter(LocalTime.now())) } .filter { it.time.isAfter(currentTime) }
return schedules.toResponse() return schedules.toResponse()
.also { .also {
@ -89,7 +93,7 @@ class ScheduleService(
fun searchSchedules(storeId: Long, date: LocalDate?, themeId: Long?): AdminScheduleSummaryListResponse { fun searchSchedules(storeId: Long, date: LocalDate?, themeId: Long?): AdminScheduleSummaryListResponse {
log.info { "[searchSchedules] 일정 검색 시작: storeId=$storeId, date=$date, themeId=$themeId" } log.info { "[searchSchedules] 일정 검색 시작: storeId=$storeId, date=$date, themeId=$themeId" }
val searchDate = date ?: LocalDate.now() val searchDate = date ?: KoreaDate.today()
val schedules: List<ScheduleOverview> = val schedules: List<ScheduleOverview> =
scheduleRepository.findStoreSchedulesWithThemeByDate(storeId, searchDate) scheduleRepository.findStoreSchedulesWithThemeByDate(storeId, searchDate)

View File

@ -1,5 +1,6 @@
package com.sangdol.roomescape.schedule.business package com.sangdol.roomescape.schedule.business
import com.sangdol.common.utils.KoreaDateTime
import com.sangdol.roomescape.schedule.exception.ScheduleErrorCode import com.sangdol.roomescape.schedule.exception.ScheduleErrorCode
import com.sangdol.roomescape.schedule.exception.ScheduleException import com.sangdol.roomescape.schedule.exception.ScheduleException
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleEntity import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleEntity
@ -13,6 +14,7 @@ import org.springframework.stereotype.Component
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
import java.time.temporal.ChronoUnit
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@ -56,9 +58,10 @@ class ScheduleValidator(
} }
private fun validateNotInPast(date: LocalDate, time: LocalTime) { private fun validateNotInPast(date: LocalDate, time: LocalTime) {
val dateTime = LocalDateTime.of(date, time) val now = KoreaDateTime.now().truncatedTo(ChronoUnit.MINUTES)
val inputDateTime = LocalDateTime.of(date, time).truncatedTo(ChronoUnit.MINUTES)
if (dateTime.isBefore(LocalDateTime.now())) { if (inputDateTime.isBefore(now)) {
log.info { log.info {
"[ScheduleValidator.validateDateTime] 이전 시간 선택으로 인한 실패: date=${date} / time=${time}" "[ScheduleValidator.validateDateTime] 이전 시간 선택으로 인한 실패: date=${date} / time=${time}"
} }

View File

@ -1,10 +1,13 @@
package com.sangdol.roomescape.schedule package com.sangdol.roomescape.schedule
import com.sangdol.roomescape.common.types.Auditor
import com.sangdol.common.types.web.HttpStatus import com.sangdol.common.types.web.HttpStatus
import com.sangdol.common.utils.KoreaDate
import com.sangdol.common.utils.KoreaDateTime
import com.sangdol.common.utils.KoreaTime
import com.sangdol.roomescape.admin.infrastructure.persistence.AdminPermissionLevel import com.sangdol.roomescape.admin.infrastructure.persistence.AdminPermissionLevel
import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType
import com.sangdol.roomescape.auth.exception.AuthErrorCode import com.sangdol.roomescape.auth.exception.AuthErrorCode
import com.sangdol.roomescape.common.types.Auditor
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.ScheduleRepository import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository
@ -54,7 +57,8 @@ class AdminScheduleApiTest(
lateinit var token: String lateinit var token: String
beforeTest { beforeTest {
val today = LocalDate.now() val now = KoreaDateTime.now()
val today = now.toLocalDate()
store = dummyInitializer.createStore() store = dummyInitializer.createStore()
val admin = AdminFixture.createStoreAdmin(storeId = store.id) val admin = AdminFixture.createStoreAdmin(storeId = store.id)
token = testAuthUtil.adminLogin(admin).second token = testAuthUtil.adminLogin(admin).second
@ -66,21 +70,21 @@ class AdminScheduleApiTest(
storeId = store.id, storeId = store.id,
request = ScheduleFixture.createRequest.copy( request = ScheduleFixture.createRequest.copy(
date = today, date = today,
time = LocalTime.now().plusHours(2) time = now.toLocalTime().plusHours(2)
) )
), ),
dummyInitializer.createSchedule( dummyInitializer.createSchedule(
storeId = store.id, storeId = store.id,
request = ScheduleFixture.createRequest.copy( request = ScheduleFixture.createRequest.copy(
date = today, date = today,
time = LocalTime.now().plusHours(1) time = now.toLocalTime().plusHours(1)
) )
), ),
dummyInitializer.createSchedule( dummyInitializer.createSchedule(
storeId = store.id, storeId = store.id,
request = ScheduleFixture.createRequest.copy( request = ScheduleFixture.createRequest.copy(
date = today.plusDays(1), date = today.plusDays(1),
time = LocalTime.of(11, 0) time = LocalTime.of(10, 0)
) )
) )
) )
@ -95,7 +99,7 @@ class AdminScheduleApiTest(
}, },
expect = { expect = {
statusCode(HttpStatus.OK.value()) statusCode(HttpStatus.OK.value())
body("data.schedules.size()", equalTo(schedules.filter { it.date.isEqual(LocalDate.now()) }.size)) body("data.schedules.size()", equalTo(schedules.filter { it.date.isEqual(KoreaDate.today()) }.size))
assertProperties( assertProperties(
props = setOf("id", "themeName", "startFrom", "endAt", "status"), props = setOf("id", "themeName", "startFrom", "endAt", "status"),
propsNameIfList = "schedules" propsNameIfList = "schedules"
@ -386,8 +390,9 @@ class AdminScheduleApiTest(
test("과거 시간을 선택하면 실패한다.") { test("과거 시간을 선택하면 실패한다.") {
val (admin, token) = testAuthUtil.defaultStoreAdminLogin() val (admin, token) = testAuthUtil.defaultStoreAdminLogin()
val date = LocalDate.now() val now = KoreaDateTime.now()
val time = LocalTime.now().minusMinutes(1) val date = now.toLocalDate()
val time = now.toLocalTime().minusMinutes(1)
val theme = dummyInitializer.createTheme() val theme = dummyInitializer.createTheme()
val request = ScheduleFixture.createRequest.copy(date = date, time = time, themeId = theme.id) val request = ScheduleFixture.createRequest.copy(date = date, time = time, themeId = theme.id)
@ -490,7 +495,8 @@ class AdminScheduleApiTest(
} }
context("정상 응답") { context("정상 응답") {
test("시간만 변경한다.") { context("시간만 변경한다.") {
test("성공") {
val (admin, token) = testAuthUtil.defaultStoreAdminLogin() val (admin, token) = testAuthUtil.defaultStoreAdminLogin()
val schedule = initialize("수정을 위한 일정 생성") { val schedule = initialize("수정을 위한 일정 생성") {
dummyInitializer.createSchedule() dummyInitializer.createSchedule()
@ -516,6 +522,26 @@ class AdminScheduleApiTest(
} }
} }
test("지난 시간을 선택하면 실패한다.") {
val (admin, token) = testAuthUtil.defaultStoreAdminLogin()
val schedule = initialize("수정을 위한 일정 생성") {
val request = ScheduleFixture.createRequest.copy(
date = KoreaDate.today(),
time = KoreaTime.now().plusHours(1)
)
dummyInitializer.createSchedule(request = request)
}
runExceptionTest(
token = token,
method = HttpMethod.PATCH,
endpoint = "/admin/schedules/${schedule.id}",
requestBody = ScheduleUpdateRequest(time = KoreaTime.now().minusMinutes(1)),
expectedErrorCode = ScheduleErrorCode.PAST_DATE_TIME
)
}
}
test("상태만 변경한다.") { test("상태만 변경한다.") {
val (admin, token) = testAuthUtil.defaultStoreAdminLogin() val (admin, token) = testAuthUtil.defaultStoreAdminLogin()
val schedule = initialize("수정을 위한 일정 생성") { val schedule = initialize("수정을 위한 일정 생성") {

View File

@ -1,6 +1,8 @@
package com.sangdol.roomescape.schedule package com.sangdol.roomescape.schedule
import com.sangdol.common.types.web.HttpStatus import com.sangdol.common.types.web.HttpStatus
import com.sangdol.common.utils.KoreaDate
import com.sangdol.common.utils.KoreaTime
import com.sangdol.roomescape.admin.infrastructure.persistence.AdminPermissionLevel import com.sangdol.roomescape.admin.infrastructure.persistence.AdminPermissionLevel
import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType
import com.sangdol.roomescape.auth.exception.AuthErrorCode import com.sangdol.roomescape.auth.exception.AuthErrorCode
@ -13,16 +15,18 @@ import org.hamcrest.CoreMatchers.equalTo
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpMethod import org.springframework.http.HttpMethod
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalTime
class ScheduleApiTest( class ScheduleApiTest(
private val scheduleRepository: ScheduleRepository private val scheduleRepository: ScheduleRepository
) : FunSpecSpringbootTest() { ) : FunSpecSpringbootTest() {
init { init {
context("특정 매장 + 날짜의 일정 및 테마 정보를 조회한다.") { context("특정 매장 + 날짜의 일정 및 테마 정보를 조회한다.") {
/**
* @throws 23 57 ~ 59분에 실행하면 실패함.
*/
test("날짜가 당일이면 현재 시간 이후의 정보만 조회된다.") { test("날짜가 당일이면 현재 시간 이후의 정보만 조회된다.") {
val size = 3 val size = 3
val date = LocalDate.now() val date = KoreaDate.today()
val store = dummyInitializer.createStore() val store = dummyInitializer.createStore()
initialize("조회를 위한 오늘 날짜의 현재 시간 이후인 ${size}개의 일정, 현재 시간 이전인 1개의 일정 생성") { initialize("조회를 위한 오늘 날짜의 현재 시간 이후인 ${size}개의 일정, 현재 시간 이전인 1개의 일정 생성") {
@ -31,7 +35,7 @@ class ScheduleApiTest(
storeId = store.id, storeId = store.id,
request = ScheduleFixture.createRequest.copy( request = ScheduleFixture.createRequest.copy(
date = date, date = date,
time = LocalTime.now().plusMinutes(i.toLong()) time = KoreaTime.now().plusMinutes(i.toLong())
) )
) )
} }
@ -40,22 +44,18 @@ class ScheduleApiTest(
storeId = store.id, storeId = store.id,
request = ScheduleFixture.createRequest.copy( request = ScheduleFixture.createRequest.copy(
date = date, date = date,
time = LocalTime.now().minusMinutes(1) time = KoreaTime.now().minusMinutes(1)
) )
) )
} }
val expectedSize = scheduleRepository.findAll().takeIf { it.isNotEmpty() }
?.let { it.count { schedule -> schedule.date.isEqual(date) && schedule.time.isAfter(LocalTime.now()) } }
?: throw AssertionError("initialize 작업에서 레코드가 저장되지 않음.")
runTest( runTest(
on = { on = {
get("/stores/${store.id}/schedules?date=${date}") get("/stores/${store.id}/schedules?date=${date}")
}, },
expect = { expect = {
statusCode(HttpStatus.OK.value()) statusCode(HttpStatus.OK.value())
body("data.schedules.size()", equalTo(expectedSize)) body("data.schedules.size()", equalTo(size))
assertProperties( assertProperties(
props = setOf("id", "startFrom", "endAt", "themeId", "themeName", "themeDifficulty", "status"), props = setOf("id", "startFrom", "endAt", "themeId", "themeName", "themeDifficulty", "status"),
propsNameIfList = "schedules" propsNameIfList = "schedules"