[#30] 코드 구조 개선 #31

Merged
pricelees merged 31 commits from refactor/#30 into main 2025-08-06 10:16:08 +00:00
9 changed files with 277 additions and 83 deletions
Showing only changes of commit 6d56f035c3 - Show all commits

View File

@ -0,0 +1,34 @@
package roomescape.reservation.implement
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Component
import roomescape.reservation.infrastructure.persistence.ReservationEntity
import roomescape.reservation.infrastructure.persistence.ReservationRepository
import roomescape.theme.infrastructure.persistence.ThemeEntity
import roomescape.time.infrastructure.persistence.TimeEntity
import java.time.LocalDate
private val log: KLogger = KotlinLogging.logger {}
@Component
class ReservationFinder(
private val reservationRepository: ReservationRepository
) {
fun isTimeReserved(time: TimeEntity): Boolean {
log.debug { "[ReservationFinder.isTimeReserved] 시작: timeId=${time.id}, startAt=${time.startAt}" }
return reservationRepository.existsByTime(time)
.also { log.debug { "[ReservationFinder.isTimeReserved] 완료: isExist=$it, timeId=${time.id}, startAt=${time.startAt}" } }
}
fun findAllByDateAndTheme(
date: LocalDate, theme: ThemeEntity
): List<ReservationEntity> {
log.debug { "[ReservationFinder.findAllByDateAndTheme] 시작: date=$date, themeId=${theme.id}" }
return reservationRepository.findAllByDateAndTheme(date, theme)
.also { log.debug { "[ReservationFinder.findAllByDateAndTheme] ${it.size}개 조회 완료: date=$date, themeId=${theme.id}" } }
}
}

View File

@ -0,0 +1,11 @@
package roomescape.reservation.implement
import org.springframework.stereotype.Component
import roomescape.reservation.infrastructure.persistence.ReservationRepository
@Component
class ReservationValidator(
private val reservationRepository: ReservationRepository
) {
}

View File

@ -6,12 +6,14 @@ import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import roomescape.reservation.web.MyReservationRetrieveResponse
import roomescape.theme.infrastructure.persistence.ThemeEntity
import roomescape.time.infrastructure.persistence.TimeEntity
import java.time.LocalDate
interface ReservationRepository
: JpaRepository<ReservationEntity, Long>, JpaSpecificationExecutor<ReservationEntity> {
fun findAllByTime(time: TimeEntity): List<ReservationEntity>
fun existsByTime(time: TimeEntity): Boolean
fun findByDateAndThemeId(date: LocalDate, themeId: Long): List<ReservationEntity>
@ -20,11 +22,11 @@ interface ReservationRepository
"""
UPDATE ReservationEntity r
SET r.status = :status
WHERE r.id = :id
WHERE r._id = :_id
"""
)
fun updateStatusByReservationId(
@Param(value = "id") reservationId: Long,
@Param(value = "_id") reservationId: Long,
@Param(value = "status") statusForChange: ReservationStatus
): Int
@ -33,28 +35,28 @@ interface ReservationRepository
SELECT EXISTS (
SELECT 1
FROM ReservationEntity r2
WHERE r2.id = :id
WHERE r2._id = :_id
AND EXISTS (
SELECT 1 FROM ReservationEntity r
WHERE r.theme.id = r2.theme.id
AND r.time.id = r2.time.id
WHERE r.theme._id = r2.theme._id
AND r.time._id = r2.time._id
AND r.date = r2.date
AND r.status != 'WAITING'
)
)
"""
)
fun isExistConfirmedReservation(@Param("id") reservationId: Long): Boolean
fun isExistConfirmedReservation(@Param("_id") reservationId: Long): Boolean
@Query(
"""
SELECT new roomescape.reservation.web.MyReservationRetrieveResponse(
r.id,
r._id,
t.name,
r.date,
r.time.startAt,
r.status,
(SELECT COUNT (r2) * 1L FROM ReservationEntity r2 WHERE r2.theme = r.theme AND r2.date = r.date AND r2.time = r.time AND r2.id < r.id),
(SELECT COUNT (r2) * 1L FROM ReservationEntity r2 WHERE r2.theme = r.theme AND r2.date = r.date AND r2.time = r.time AND r2._id < r._id),
p.paymentKey,
p.totalAmount
)
@ -62,8 +64,9 @@ interface ReservationRepository
JOIN r.theme t
LEFT JOIN PaymentEntity p
ON p.reservation = r
WHERE r.member.id = :memberId
WHERE r.member._id = :memberId
"""
)
fun findAllByMemberId(memberId: Long): List<MyReservationRetrieveResponse>
fun findAllByDateAndTheme(date: LocalDate, theme: ThemeEntity): List<ReservationEntity>
}

View File

@ -0,0 +1,19 @@
package roomescape.theme.business.domain
import roomescape.time.web.TimeWithAvailabilityResponse
import java.time.LocalDate
import java.time.LocalTime
class TimeWithAvailability(
private val timeId: Long,
private val startAt: LocalTime,
private val date: LocalDate,
private val themeId: Long,
private val isReservable: Boolean
) {
fun toResponse() = TimeWithAvailabilityResponse(
id = timeId,
startAt = startAt,
isAvailable = isReservable
)
}

View File

@ -0,0 +1,28 @@
package roomescape.theme.implement
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Component
import roomescape.theme.exception.ThemeErrorCode
import roomescape.theme.exception.ThemeException
import roomescape.theme.infrastructure.persistence.ThemeEntity
import roomescape.theme.infrastructure.persistence.ThemeRepository
private val log: KLogger = KotlinLogging.logger {}
@Component
class ThemeFinder(
private val themeRepository: ThemeRepository
) {
fun findById(id: Long): ThemeEntity {
log.debug { "[ThemeFinder.findById] 조회 시작: memberId=$id" }
return themeRepository.findByIdOrNull(id)
?.also { log.debug { "[ThemeFinder.findById] 조회 완료: id=$id, name=${it.name}" } }
?: run {
log.warn { "[ThemeFinder.findById] 조회 실패: id=$id" }
throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
}
}
}

View File

@ -1,17 +1,12 @@
package roomescape.time.business
import com.github.f4b6a3.tsid.TsidFactory
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.reservation.infrastructure.persistence.ReservationEntity
import roomescape.reservation.infrastructure.persistence.ReservationRepository
import roomescape.time.exception.TimeErrorCode
import roomescape.time.exception.TimeException
import roomescape.theme.business.domain.TimeWithAvailability
import roomescape.time.implement.TimeFinder
import roomescape.time.implement.TimeWriter
import roomescape.time.infrastructure.persistence.TimeEntity
import roomescape.time.infrastructure.persistence.TimeRepository
import roomescape.time.web.*
import java.time.LocalDate
import java.time.LocalTime
@ -20,87 +15,53 @@ private val log = KotlinLogging.logger {}
@Service
class TimeService(
private val tsidFactory: TsidFactory,
private val timeRepository: TimeRepository,
private val reservationRepository: ReservationRepository,
private val timeFinder: TimeFinder,
private val timeWriter: TimeWriter,
) {
@Transactional(readOnly = true)
fun findById(id: Long): TimeEntity {
log.debug { "[TimeService.findById] 시간 조회 시작: timeId=$id" }
return timeRepository.findByIdOrNull(id)
?.also { log.info { "[TimeService.findById] 시간 조회 완료: timeId=$id" } }
?: run {
log.warn { "[TimeService.findById] 시간 조회 실패: timeId=$id" }
throw TimeException(TimeErrorCode.TIME_NOT_FOUND)
}
log.info { "[TimeService.findById] 시작: timeId=$id" }
return timeFinder.findById(id)
.also { log.info { "[TimeService.findById] 완료: timeId=$id, startAt=${it.startAt}" } }
}
@Transactional(readOnly = true)
fun findTimes(): TimeRetrieveListResponse {
log.debug { "[TimeService.findTimes] 모든 시간 조회 시작" }
return timeRepository.findAll()
.also { log.info { "[TimeService.findTimes] ${it.size}개의 시간 조회 완료" } }
log.info { "[TimeService.findTimes] 시작" }
return timeFinder.findAll()
.toResponse()
}
@Transactional
fun createTime(request: TimeCreateRequest): TimeCreateResponse {
log.debug { "[TimeService.createTime] 시간 생성 시작: startAt=${request.startAt}" }
val startAt: LocalTime = request.startAt
if (timeRepository.existsByStartAt(startAt)) {
log.info { "[TimeService.createTime] 시간 생성 실패(시간 중복): startAt=$startAt" }
throw TimeException(TimeErrorCode.TIME_DUPLICATED)
}
val time = TimeEntity(
_id = tsidFactory.next(),
startAt = request.startAt
)
return timeRepository.save(time)
.also { log.info { "[TimeService.createTime] 시간 생성 완료: timeId=${it.id}" } }
.toCreateResponse()
}
@Transactional
fun deleteTime(id: Long) {
log.debug { "[TimeService.deleteTime] 시간 삭제 시작: timeId=$id" }
val time: TimeEntity = findById(id)
log.debug { "[TimeService.deleteTime] 시간이 ${time.startAt}인 모든 예약 조회 시작" }
val reservations: List<ReservationEntity> = reservationRepository.findAllByTime(time)
log.debug { "[TimeService.deleteTime] 시간이 ${time.startAt}인 모든 ${reservations.size} 개의 예약 조회 완료" }
if (reservations.isNotEmpty()) {
log.info { "[TimeService.deleteTime] 시간 삭제 실패(예약이 있는 시간): timeId=$id" }
throw TimeException(TimeErrorCode.TIME_ALREADY_RESERVED)
}
timeRepository.delete(time)
.also { log.info { "[TimeService.deleteTime] 시간 삭제 완료: timeId=$id" } }
.also { log.info { "[TimeService.findTimes] 완료. ${it.times.size}개 반환" } }
}
@Transactional(readOnly = true)
fun findTimesWithAvailability(date: LocalDate, themeId: Long): TimeWithAvailabilityListResponse {
log.debug { "[TimeService.findTimesWithAvailability] 예약 가능 시간 조회 시작: date=$date, themeId=$themeId" }
log.info { "[TimeService.findTimesWithAvailability] 시작: date=$date, themeId=$themeId" }
log.debug { "[TimeService.findTimesWithAvailability] 모든 시간 조회 " }
val allTimes = timeRepository.findAll()
log.debug { "[TimeService.findTimesWithAvailability] ${allTimes.size}개의 시간 조회 완료" }
val times: List<TimeWithAvailability> = timeFinder.findAllWithAvailabilityByDateAndThemeId(date, themeId)
log.debug { "[TimeService.findTimesWithAvailability] date=$date, themeId=$themeId 인 모든 예약 조회 시작" }
val reservations: List<ReservationEntity> = reservationRepository.findByDateAndThemeId(date, themeId)
log.debug { "[TimeService.findTimesWithAvailability] date=$date, themeId=$themeId${reservations.size} 개의 예약 조회 완료" }
return TimeWithAvailabilityListResponse(times.map { it.toResponse() })
.also { log.info { "[TimeService.findTimesWithAvailability] ${it.times.size}개 반환: date=$date, themeId=$themeId" } }
}
@Transactional
fun createTime(request: TimeCreateRequest): TimeCreateResponse {
val startAt: LocalTime = request.startAt
log.info { "[TimeService.createTime] 시작: startAt=${startAt}" }
return TimeWithAvailabilityListResponse(allTimes.map { time ->
val isAvailable: Boolean = reservations.none { reservation -> reservation.time.id == time.id }
TimeWithAvailabilityResponse(time.id!!, time.startAt, isAvailable)
}).also {
log.info {
"[TimeService.findTimesWithAvailability] date=$date, themeId=$themeId 에 대한 예약 가능 여부가 담긴 모든 시간 조회 완료"
}
}
return timeWriter.create(startAt)
.toCreateResponse()
.also { log.info { "[TimeService.createTime] 완료: startAt=${startAt}, timeId=${it.id}" } }
}
@Transactional
fun deleteTime(id: Long) {
log.info { "[TimeService.deleteTime] 시작: timeId=$id" }
val time: TimeEntity = timeFinder.findById(id)
timeWriter.delete(time)
.also { log.info { "[TimeService.deleteTime] 완료: timeId=$id, startAt=${time.startAt}" } }
}
}

View File

@ -0,0 +1,60 @@
package roomescape.time.implement
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Component
import roomescape.reservation.implement.ReservationFinder
import roomescape.reservation.infrastructure.persistence.ReservationEntity
import roomescape.theme.business.domain.TimeWithAvailability
import roomescape.theme.implement.ThemeFinder
import roomescape.time.exception.TimeErrorCode
import roomescape.time.exception.TimeException
import roomescape.time.infrastructure.persistence.TimeEntity
import roomescape.time.infrastructure.persistence.TimeRepository
import java.time.LocalDate
private val log: KLogger = KotlinLogging.logger {}
@Component
class TimeFinder(
private val timeRepository: TimeRepository,
private val reservationFinder: ReservationFinder,
private val themeFinder: ThemeFinder
) {
fun findAll(): List<TimeEntity> {
log.debug { "[TimeFinder.findAll] 시작" }
return timeRepository.findAll()
.also { log.debug { "[TimeFinder.findAll] ${it.size}개 시간 조회 완료" } }
}
fun findById(id: Long): TimeEntity {
log.debug { "[TimeFinder.findById] 조회 시작: timeId=$id" }
return timeRepository.findByIdOrNull(id)
?.also { log.debug { "[TimeFinder.findById] 조회 완료: timeId=$id, startAt=${it.startAt}" } }
?: run {
log.warn { "[TimeFinder.findById] 조회 실패: timeId=$id" }
throw TimeException(TimeErrorCode.TIME_NOT_FOUND)
}
}
fun findAllWithAvailabilityByDateAndThemeId(
date: LocalDate, themeId: Long
): List<TimeWithAvailability> {
log.debug { "[TimeFinder.findAllWithAvailabilityByDateAndThemeId] 조회 시작: date:$date, themeId=$themeId" }
val theme = themeFinder.findById(themeId)
val reservations: List<ReservationEntity> = reservationFinder.findAllByDateAndTheme(date, theme)
val allTimes: List<TimeEntity> = findAll()
return allTimes.map { time ->
val isReservable: Boolean = reservations.any { reservation -> time.id == reservation.id }
TimeWithAvailability(time.id!!, time.startAt, date, themeId, isReservable)
}.also {
log.debug { "[TimeFinder.findAllWithAvailabilityByDateAndThemeId] ${it.size}개 조회 완료: date:$date, themeId=$themeId" }
}
}
}

View File

@ -0,0 +1,41 @@
package roomescape.time.implement
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Component
import roomescape.reservation.implement.ReservationFinder
import roomescape.time.exception.TimeErrorCode
import roomescape.time.exception.TimeException
import roomescape.time.infrastructure.persistence.TimeEntity
import roomescape.time.infrastructure.persistence.TimeRepository
import java.time.LocalTime
private val log: KLogger = KotlinLogging.logger {}
@Component
class TimeValidator(
private val timeRepository: TimeRepository,
private val reservationFinder: ReservationFinder
) {
fun validateIsAlreadyExists(startAt: LocalTime) {
log.debug { "[TimeValidator.validateIsAlreadyExists] 시작: startAt=${startAt}" }
if (timeRepository.existsByStartAt(startAt)) {
log.info { "[TimeValidator.validateIsAlreadyExists] 중복 시간: startAt=$startAt" }
throw TimeException(TimeErrorCode.TIME_DUPLICATED)
}
log.debug { "[TimeValidator.validateIsAlreadyExists] 완료: startAt=${startAt}" }
}
fun validateIsReserved(time: TimeEntity) {
log.debug { "[TimeValidator.validateIsReserved] 시작: id=${time.id}, startAt=${time.startAt}" }
if (reservationFinder.isTimeReserved(time)) {
log.info { "[TimeValidator.validateIsReserved] 예약이 있는 시간: timeId=${time.id}, startAt=${time.startAt}" }
throw TimeException(TimeErrorCode.TIME_ALREADY_RESERVED)
}
log.debug { "[TimeValidator.validateIsReserved] 시작: id=${time.id}, startAt=${time.startAt}" }
}
}

View File

@ -0,0 +1,37 @@
package roomescape.time.implement
import com.github.f4b6a3.tsid.TsidFactory
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Component
import roomescape.common.config.next
import roomescape.time.infrastructure.persistence.TimeEntity
import roomescape.time.infrastructure.persistence.TimeRepository
import java.time.LocalTime
private val log: KLogger = KotlinLogging.logger {}
@Component
class TimeWriter(
private val timeValidator: TimeValidator,
private val timeRepository: TimeRepository,
private val tsidFactory: TsidFactory
) {
fun create(startAt: LocalTime): TimeEntity {
log.debug { "[TimeWriter.create] 시작: startAt=$startAt" }
timeValidator.validateIsAlreadyExists(startAt)
val time = TimeEntity(_id = tsidFactory.next(), startAt = startAt)
return timeRepository.save(time)
.also { log.debug { "[TimeWriter.create] 완료: startAt=$startAt, id=${it.id}" } }
}
fun delete(time: TimeEntity) {
log.debug { "[TimeWriter.delete] 시작: id=${time.id}" }
timeValidator.validateIsReserved(time)
timeRepository.delete(time)
.also { log.debug { "[TimeWriter.delete] 완료: id=${time.id}, startAt=${time.startAt}" } }
}
}