[#16] Reservation 도메인 코드 코틀린 마이그레이션 #17

Merged
pricelees merged 40 commits from refactor/#16 into main 2025-07-21 12:08:56 +00:00
2 changed files with 221 additions and 194 deletions
Showing only changes of commit 036947153d - Show all commits

View File

@ -1,227 +1,244 @@
package roomescape.reservation.business; package roomescape.reservation.business
import java.time.LocalDate; import org.springframework.data.jpa.domain.Specification
import java.time.LocalDateTime; import org.springframework.data.repository.findByIdOrNull
import java.util.List; import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import org.springframework.data.jpa.domain.Specification; import org.springframework.transaction.annotation.Transactional
import org.springframework.http.HttpStatus; import roomescape.common.exception.ErrorType
import org.springframework.stereotype.Service; import roomescape.common.exception.RoomescapeException
import org.springframework.transaction.annotation.Transactional; import roomescape.member.business.MemberService
import roomescape.reservation.infrastructure.persistence.*
import roomescape.common.exception.ErrorType; import roomescape.reservation.web.*
import roomescape.common.exception.RoomescapeException; import roomescape.theme.business.ThemeService
import roomescape.member.business.MemberService; import java.time.LocalDate
import roomescape.member.infrastructure.persistence.MemberEntity; import java.time.LocalDateTime
import roomescape.reservation.infrastructure.persistence.ReservationEntity;
import roomescape.reservation.infrastructure.persistence.ReservationRepository;
import roomescape.reservation.infrastructure.persistence.ReservationSearchSpecification;
import roomescape.reservation.infrastructure.persistence.ReservationStatus;
import roomescape.reservation.infrastructure.persistence.ReservationTimeEntity;
import roomescape.reservation.web.AdminReservationRequest;
import roomescape.reservation.web.MyReservationsResponse;
import roomescape.reservation.web.ReservationRequest;
import roomescape.reservation.web.ReservationResponse;
import roomescape.reservation.web.ReservationsResponse;
import roomescape.reservation.web.WaitingRequest;
import roomescape.theme.business.ThemeService;
import roomescape.theme.infrastructure.persistence.ThemeEntity;
@Service @Service
@Transactional @Transactional
public class ReservationService { class ReservationService(
private val reservationRepository: ReservationRepository,
private val reservationTimeService: ReservationTimeService,
private val memberService: MemberService,
private val themeService: ThemeService,
) {
private final ReservationRepository reservationRepository; @Transactional(readOnly = true)
private final ReservationTimeService reservationTimeService; fun findAllReservations(): ReservationsResponse {
private final MemberService memberService; val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
private final ThemeService themeService; .confirmed()
.build()
public ReservationService(
ReservationRepository reservationRepository,
ReservationTimeService reservationTimeService,
MemberService memberService,
ThemeService themeService
) {
this.reservationRepository = reservationRepository;
this.reservationTimeService = reservationTimeService;
this.memberService = memberService;
this.themeService = themeService;
}
@Transactional(readOnly = true) return ReservationsResponse(findAllReservationByStatus(spec))
public ReservationsResponse findAllReservations() { }
Specification<ReservationEntity> spec = new ReservationSearchSpecification().confirmed().build();
List<ReservationResponse> response = findAllReservationByStatus(spec);
return new ReservationsResponse(response); @Transactional(readOnly = true)
} fun findAllWaiting(): ReservationsResponse {
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
.waiting()
.build()
@Transactional(readOnly = true) return ReservationsResponse(findAllReservationByStatus(spec))
public ReservationsResponse findAllWaiting() { }
Specification<ReservationEntity> spec = new ReservationSearchSpecification().waiting().build();
List<ReservationResponse> response = findAllReservationByStatus(spec);
return new ReservationsResponse(response); private fun findAllReservationByStatus(spec: Specification<ReservationEntity>): List<ReservationResponse> {
} return reservationRepository.findAll(spec).map { it.toResponse() }
}
private List<ReservationResponse> findAllReservationByStatus(Specification<ReservationEntity> spec) { fun removeReservationById(reservationId: Long, memberId: Long) {
return reservationRepository.findAll(spec) validateIsMemberAdmin(memberId)
.stream() reservationRepository.deleteById(reservationId)
.map(ReservationResponse::from) }
.toList();
}
public void removeReservationById(Long reservationId, Long memberId) { fun addReservation(request: ReservationRequest, memberId: Long): ReservationEntity {
validateIsMemberAdmin(memberId); validateIsReservationExist(request.themeId, request.timeId, request.date)
reservationRepository.deleteById(reservationId); return getReservationForSave(
} request.timeId,
request.themeId,
request.date,
memberId,
ReservationStatus.CONFIRMED
).also {
reservationRepository.save(it)
}
}
public ReservationEntity addReservation(ReservationRequest request, Long memberId) { fun addReservationByAdmin(request: AdminReservationRequest): ReservationResponse {
validateIsReservationExist(request.themeId, request.timeId, request.date); validateIsReservationExist(request.themeId, request.timeId, request.date)
ReservationEntity reservation = getReservationForSave(request.timeId, request.themeId, request.date, memberId,
ReservationStatus.CONFIRMED);
return reservationRepository.save(reservation);
}
public ReservationResponse addReservationByAdmin(AdminReservationRequest request) { return addReservationWithoutPayment(
validateIsReservationExist(request.themeId, request.timeId, request.date); request.themeId,
return addReservationWithoutPayment(request.themeId, request.timeId, request.date, request.timeId,
request.memberId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED); request.date,
} request.memberId,
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
)
}
public ReservationResponse addWaiting(WaitingRequest request, Long memberId) { fun addWaiting(request: WaitingRequest, memberId: Long): ReservationResponse {
validateMemberAlreadyReserve(request.themeId, request.timeId, request.date, memberId); validateMemberAlreadyReserve(request.themeId, request.timeId, request.date, memberId)
return addReservationWithoutPayment(request.themeId, request.timeId, request.date, memberId, return addReservationWithoutPayment(
ReservationStatus.WAITING); request.themeId,
} request.timeId,
request.date,
memberId,
ReservationStatus.WAITING
)
}
private ReservationResponse addReservationWithoutPayment(Long themeId, Long timeId, LocalDate date, Long memberId, private fun addReservationWithoutPayment(
ReservationStatus status) { themeId: Long,
ReservationEntity reservation = getReservationForSave(timeId, themeId, date, memberId, status); timeId: Long,
ReservationEntity saved = reservationRepository.save(reservation); date: LocalDate,
return ReservationResponse.from(saved); memberId: Long,
} status: ReservationStatus
): ReservationResponse = getReservationForSave(timeId, themeId, date, memberId, status)
.also {
reservationRepository.save(it)
}.toResponse()
private void validateMemberAlreadyReserve(Long themeId, Long timeId, LocalDate date, Long memberId) {
Specification<ReservationEntity> spec = new ReservationSearchSpecification()
.sameMemberId(memberId)
.sameThemeId(themeId)
.sameTimeId(timeId)
.sameDate(date)
.build();
if (reservationRepository.exists(spec)) { private fun validateMemberAlreadyReserve(themeId: Long?, timeId: Long?, date: LocalDate?, memberId: Long?) {
throw new RoomescapeException(ErrorType.HAS_RESERVATION_OR_WAITING, HttpStatus.BAD_REQUEST); val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
} .sameMemberId(memberId)
} .sameThemeId(themeId)
.sameTimeId(timeId)
.sameDate(date)
.build()
private void validateIsReservationExist(Long themeId, Long timeId, LocalDate date) { if (reservationRepository.exists(spec)) {
Specification<ReservationEntity> spec = new ReservationSearchSpecification() throw RoomescapeException(ErrorType.HAS_RESERVATION_OR_WAITING, HttpStatus.BAD_REQUEST)
.confirmed() }
.sameThemeId(themeId) }
.sameTimeId(timeId)
.sameDate(date)
.build();
if (reservationRepository.exists(spec)) { private fun validateIsReservationExist(themeId: Long, timeId: Long, date: LocalDate) {
throw new RoomescapeException(ErrorType.RESERVATION_DUPLICATED, HttpStatus.CONFLICT); val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
} .confirmed()
} .sameThemeId(themeId)
.sameTimeId(timeId)
.sameDate(date)
.build()
private void validateDateAndTime( if (reservationRepository.exists(spec)) {
LocalDate requestDate, throw RoomescapeException(ErrorType.RESERVATION_DUPLICATED, HttpStatus.CONFLICT)
ReservationTimeEntity requestReservationTime }
) { }
LocalDateTime now = LocalDateTime.now();
LocalDateTime request = LocalDateTime.of(requestDate, requestReservationTime.getStartAt());
if (request.isBefore(now)) {
throw new RoomescapeException(ErrorType.RESERVATION_PERIOD_IN_PAST,
String.format("[now: %s %s | request: %s %s]",
now.toLocalDate(), now.toLocalTime(), requestDate, requestReservationTime.getStartAt()),
HttpStatus.BAD_REQUEST
);
}
}
private ReservationEntity getReservationForSave(Long timeId, Long themeId, LocalDate date, Long memberId, private fun validateDateAndTime(
ReservationStatus status) { requestDate: LocalDate,
ReservationTimeEntity time = reservationTimeService.findTimeById(timeId); requestReservationTime: ReservationTimeEntity
ThemeEntity theme = themeService.findThemeById(themeId); ) {
MemberEntity member = memberService.findById(memberId); val now = LocalDateTime.now()
val request = LocalDateTime.of(requestDate, requestReservationTime.startAt)
validateDateAndTime(date, time); if (request.isBefore(now)) {
return new ReservationEntity(null, date, time, theme, member, status); throw RoomescapeException(
} ErrorType.RESERVATION_PERIOD_IN_PAST,
"[now: $now | request: $request]",
HttpStatus.BAD_REQUEST
)
}
}
@Transactional(readOnly = true) private fun getReservationForSave(
public ReservationsResponse findFilteredReservations(Long themeId, Long memberId, LocalDate dateFrom, timeId: Long,
LocalDate dateTo) { themeId: Long,
validateDateForSearch(dateFrom, dateTo); date: LocalDate,
Specification<ReservationEntity> spec = new ReservationSearchSpecification() memberId: Long,
.confirmed() status: ReservationStatus
.sameThemeId(themeId) ): ReservationEntity {
.sameMemberId(memberId) val time = reservationTimeService.findTimeById(timeId)
.dateStartFrom(dateFrom) val theme = themeService.findThemeById(themeId)
.dateEndAt(dateTo) val member = memberService.findById(memberId)
.build();
List<ReservationResponse> response = reservationRepository.findAll(spec) validateDateAndTime(date, time)
.stream()
.map(ReservationResponse::from)
.toList();
return new ReservationsResponse(response); return ReservationEntity(
} date = date,
reservationTime = time,
theme = theme,
member = member,
reservationStatus = status
)
}
private void validateDateForSearch(LocalDate startFrom, LocalDate endAt) { @Transactional(readOnly = true)
if (startFrom == null || endAt == null) { fun findFilteredReservations(
return; themeId: Long?,
} memberId: Long?,
if (startFrom.isAfter(endAt)) { dateFrom: LocalDate?,
throw new RoomescapeException(ErrorType.INVALID_DATE_RANGE, dateTo: LocalDate?
String.format("[startFrom: %s, endAt: %s", startFrom, endAt), HttpStatus.BAD_REQUEST); ): ReservationsResponse {
} validateDateForSearch(dateFrom, dateTo)
} val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
.confirmed()
.sameThemeId(themeId)
.sameMemberId(memberId)
.dateStartFrom(dateFrom)
.dateEndAt(dateTo)
.build()
@Transactional(readOnly = true) return ReservationsResponse(findAllReservationByStatus(spec))
public MyReservationsResponse findMemberReservations(Long memberId) { }
return new MyReservationsResponse(reservationRepository.findMyReservations(memberId));
}
public void approveWaiting(Long reservationId, Long memberId) { private fun validateDateForSearch(startFrom: LocalDate?, endAt: LocalDate?) {
validateIsMemberAdmin(memberId); if (startFrom == null || endAt == null) {
if (reservationRepository.isExistConfirmedReservation(reservationId)) { return
throw new RoomescapeException(ErrorType.RESERVATION_DUPLICATED, HttpStatus.CONFLICT); }
} if (startFrom.isAfter(endAt)) {
reservationRepository.updateStatusByReservationId(reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED); throw RoomescapeException(
} ErrorType.INVALID_DATE_RANGE,
"[startFrom: $startFrom, endAt: $endAt", HttpStatus.BAD_REQUEST
)
}
}
public void cancelWaiting(Long reservationId, Long memberId) { @Transactional(readOnly = true)
ReservationEntity waiting = reservationRepository.findById(reservationId) fun findMemberReservations(memberId: Long): MyReservationsResponse {
.filter(ReservationEntity::isWaiting) return MyReservationsResponse(reservationRepository.findMyReservations(memberId))
.filter(r -> r.isSameMember(memberId)) }
.orElseThrow(() -> throwReservationNotFound(reservationId));
reservationRepository.delete(waiting);
}
public void denyWaiting(Long reservationId, Long memberId) { fun approveWaiting(reservationId: Long, memberId: Long) {
validateIsMemberAdmin(memberId); validateIsMemberAdmin(memberId)
ReservationEntity waiting = reservationRepository.findById(reservationId) if (reservationRepository. isExistConfirmedReservation(reservationId)) {
.filter(ReservationEntity::isWaiting) throw RoomescapeException(ErrorType.RESERVATION_DUPLICATED, HttpStatus.CONFLICT)
.orElseThrow(() -> throwReservationNotFound(reservationId)); }
reservationRepository.delete(waiting); reservationRepository.updateStatusByReservationId(reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED)
} }
private void validateIsMemberAdmin(Long memberId) { fun cancelWaiting(reservationId: Long, memberId: Long) {
MemberEntity member = memberService.findById(memberId); reservationRepository.findByIdOrNull(reservationId)?.takeIf {
if (member.isAdmin()) { it.isWaiting() && it.isSameMember(memberId)
return; }?.let {
} reservationRepository.delete(it)
throw new RoomescapeException(ErrorType.PERMISSION_DOES_NOT_EXIST, HttpStatus.FORBIDDEN); } ?: throw throwReservationNotFound(reservationId)
} }
private RoomescapeException throwReservationNotFound(Long reservationId) { fun denyWaiting(reservationId: Long, memberId: Long) {
return new RoomescapeException(ErrorType.RESERVATION_NOT_FOUND, validateIsMemberAdmin(memberId)
String.format("[reservationId: %d]", reservationId), HttpStatus.NOT_FOUND); reservationRepository.findByIdOrNull(reservationId)?.takeIf {
} it.isWaiting()
}?.let {
reservationRepository.delete(it)
} ?: throw throwReservationNotFound(reservationId)
}
private fun validateIsMemberAdmin(memberId: Long) {
memberService.findById(memberId).takeIf {
it.isAdmin()
} ?: throw RoomescapeException(
ErrorType.PERMISSION_DOES_NOT_EXIST,
"[memberId: $memberId]",
HttpStatus.FORBIDDEN
)
}
private fun throwReservationNotFound(reservationId: Long?): RoomescapeException {
return RoomescapeException(
ErrorType.RESERVATION_NOT_FOUND,
"[reservationId: $reservationId]",
HttpStatus.NOT_FOUND
)
}
} }

View File

@ -4,9 +4,11 @@ import com.fasterxml.jackson.annotation.JsonProperty
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import roomescape.member.web.MemberResponse import roomescape.member.web.MemberResponse
import roomescape.member.web.MemberResponse.Companion.fromEntity import roomescape.member.web.MemberResponse.Companion.fromEntity
import roomescape.member.web.toResponse
import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationEntity
import roomescape.reservation.infrastructure.persistence.ReservationStatus import roomescape.reservation.infrastructure.persistence.ReservationStatus
import roomescape.theme.web.ThemeResponse import roomescape.theme.web.ThemeResponse
import roomescape.theme.web.toResponse
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalTime import java.time.LocalTime
@ -85,10 +87,18 @@ data class ReservationResponse(
} }
} }
fun ReservationEntity.toResponse(): ReservationResponse = ReservationResponse(
id = this.id!!,
date = this.date,
member = this.member.toResponse(),
time = this.reservationTime.toResponse(),
theme = this.theme.toResponse(),
status = this.reservationStatus
)
@Schema(name = "예약 목록 조회 응답", description = "모든 예약 정보 조회 응답시 사용됩니다.") @Schema(name = "예약 목록 조회 응답", description = "모든 예약 정보 조회 응답시 사용됩니다.")
@JvmRecord @JvmRecord
data class ReservationsResponse( data class ReservationsResponse(
@field:Schema(description = "모든 예약 및 대기 목록") @field:Schema(description = "모든 예약 및 대기 목록")
val reservations: List<ReservationResponse> val reservations: List<ReservationResponse>
) )