diff --git a/src/main/kotlin/roomescape/schedule/business/ScheduleService.kt b/src/main/kotlin/roomescape/schedule/business/ScheduleService.kt new file mode 100644 index 00000000..7a44d88b --- /dev/null +++ b/src/main/kotlin/roomescape/schedule/business/ScheduleService.kt @@ -0,0 +1,123 @@ +package roomescape.schedule.business + +import ScheduleException +import com.github.f4b6a3.tsid.TsidFactory +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import roomescape.common.config.next +import roomescape.member.business.MemberService +import roomescape.schedule.exception.ScheduleErrorCode +import roomescape.schedule.infrastructure.persistence.ScheduleEntity +import roomescape.schedule.infrastructure.persistence.ScheduleRepository +import roomescape.schedule.infrastructure.persistence.ScheduleStatus +import roomescape.schedule.web.* +import java.time.LocalDate + +private val log: KLogger = KotlinLogging.logger {} + +@Service +class ScheduleService( + private val scheduleRepository: ScheduleRepository, + private val scheduleValidator: ScheduleValidator, + private val tsidFactory: TsidFactory, + private val memberService: MemberService +) { + + @Transactional(readOnly = true) + fun findThemesByDate(date: LocalDate): AvailableThemeIdListResponse { + log.info { "[ScheduleService.findThemesByDate] 동일한 날짜의 모든 테마 조회: date=$date" } + + return scheduleRepository.findAllByDate(date) + .toThemeIdListResponse() + .also { + log.info { "[ScheduleService.findThemesByDate] date=${date}인 ${it.themeIds.size}개 테마 조회 완료" } + } + } + + @Transactional(readOnly = true) + fun findSchedules(date: LocalDate, themeId: Long): ScheduleRetrieveListResponse { + log.info { "[ScheduleService.findSchedules] 동일한 날짜와 테마인 모든 일정 조회: date=${date}, themeId=${themeId}" } + + return scheduleRepository.findAllByDateAndThemeId(date, themeId) + .toRetrieveListResponse() + .also { + log.info { "[ScheduleService.findSchedules] date=${date}, themeId=${themeId}인 ${it.schedules.size}개 일정 조회 완료" } + } + } + + @Transactional(readOnly = true) + fun findDetail(id: Long): ScheduleDetailRetrieveResponse { + log.info { "[ScheduleService.findDetail] 일정 상세 정보조회 시작: id=$id" } + + val schedule: ScheduleEntity = findOrThrow(id) + + val createdBy = memberService.findById(schedule.createdBy).name + val updatedBy = memberService.findById(schedule.updatedBy).name + + return schedule.toDetailRetrieveResponse(createdBy, updatedBy) + .also { + log.info { "[ScheduleService.findDetail] 일정 상세 조회 완료: id=$id" } + } + } + + @Transactional + fun createSchedule(request: ScheduleCreateRequest): ScheduleCreateResponse { + log.info { "[ScheduleService.createSchedule] 일정 생성 시작: date=${request.date}, time=${request.time}, themeId=${request.themeId}" } + + scheduleValidator.validateCanCreate(request) + + val schedule = ScheduleEntity( + id = tsidFactory.next(), + date = request.date, + time = request.time, + themeId = request.themeId, + status = ScheduleStatus.AVAILABLE + ) + + return ScheduleCreateResponse(scheduleRepository.save(schedule).id) + .also { + log.info { "[ScheduleService.createSchedule] 일정 생성 완료: id=${it.id}" } + } + } + + @Transactional + fun updateSchedule(id: Long, request: ScheduleUpdateRequest) { + log.info { "[ScheduleService.updateSchedule] 일정 수정 시작: id=$id, request=${request}" } + val schedule: ScheduleEntity = findOrThrow(id) + + scheduleValidator.validateCanUpdate(schedule, request) + + schedule.modifyIfNotNull( + request.time, + request.status + ).also { + log.info { "[ScheduleService.updateSchedule] 일정 수정 완료: id=$id, request=${request}" } + } + } + + @Transactional + fun deleteSchedule(id: Long) { + log.info { "[ScheduleService.deleteSchedule] 일정 삭제 시작: id=$id" } + + val schedule: ScheduleEntity = findOrThrow(id) + + scheduleValidator.validateCanDelete(schedule) + + scheduleRepository.delete(schedule).also { + log.info { "[ScheduleService.deleteSchedule] 일정 삭제 완료: id=$id" } + } + } + + private fun findOrThrow(id: Long): ScheduleEntity { + log.info { "[ScheduleService.findOrThrow] 일정 조회 시작: id=$id" } + + return scheduleRepository.findByIdOrNull(id) + ?: run { + log.warn { "[ScheduleService.updateSchedule] 일정 조회 실패. id=$id" } + throw ScheduleException(ScheduleErrorCode.SCHEDULE_NOT_FOUND) + } + } +} diff --git a/src/main/kotlin/roomescape/schedule/business/ScheduleValidator.kt b/src/main/kotlin/roomescape/schedule/business/ScheduleValidator.kt new file mode 100644 index 00000000..d609b9fa --- /dev/null +++ b/src/main/kotlin/roomescape/schedule/business/ScheduleValidator.kt @@ -0,0 +1,67 @@ +package roomescape.schedule.business + +import ScheduleException +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.stereotype.Component +import roomescape.schedule.exception.ScheduleErrorCode +import roomescape.schedule.infrastructure.persistence.ScheduleEntity +import roomescape.schedule.infrastructure.persistence.ScheduleRepository +import roomescape.schedule.infrastructure.persistence.ScheduleStatus +import roomescape.schedule.web.ScheduleCreateRequest +import roomescape.schedule.web.ScheduleUpdateRequest +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime + +private val log: KLogger = KotlinLogging.logger {} + +@Component +class ScheduleValidator( + private val scheduleRepository: ScheduleRepository +) { + fun validateCanDelete(schedule: ScheduleEntity) { + val status: ScheduleStatus = schedule.status + + if (status !in listOf(ScheduleStatus.AVAILABLE,ScheduleStatus.BLOCKED)) { + log.info { "[ScheduleValidator.validateCanDelete] 삭제 실패: id=${schedule.id} / status=${status}" } + throw ScheduleException(ScheduleErrorCode.SCHEDULE_IN_USE) + } + } + + fun validateCanUpdate(schedule: ScheduleEntity, request: ScheduleUpdateRequest) { + val date: LocalDate = schedule.date + val time: LocalTime = request.time ?: schedule.time + + validateDateTime(date, time) + } + + fun validateCanCreate(request: ScheduleCreateRequest) { + val date: LocalDate = request.date + val time: LocalTime = request.time + val themeId: Long = request.themeId + + validateAlreadyExists(date, themeId, time) + validateDateTime(date, time) + } + + private fun validateAlreadyExists(date: LocalDate, themeId: Long, time: LocalTime) { + if (scheduleRepository.existsByDateAndThemeIdAndTime(date, themeId, time)) { + log.info { + "[ScheduleValidator.validateAlreadyExists] 동일한 날짜, 테마, 시간 존재: date=${date} / themeId=${themeId} / time=${time}" + } + throw ScheduleException(ScheduleErrorCode.SCHEDULE_ALREADY_EXISTS) + } + } + + private fun validateDateTime(date: LocalDate, time: LocalTime) { + val dateTime = LocalDateTime.of(date, time) + + if (dateTime.isBefore(LocalDateTime.now())) { + log.info { + "[ScheduleValidator.validateDateTime] 이전 시간 선택: date=${date} / time=${time}" + } + throw ScheduleException(ScheduleErrorCode.PAST_DATE_TIME) + } + } +}