From cc256bff5736b43f150281c11ae9d8991a46827a Mon Sep 17 00:00:00 2001 From: Junhyeong Kim Date: Fri, 25 Aug 2023 00:45:15 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9D=BC=EB=B0=98=20=EC=98=88=EC=95=BD?= =?UTF-8?q?=20=EB=B0=8F=20=EC=A0=95=EA=B8=B0=20=EC=98=88=EC=95=BD=20API=20?= =?UTF-8?q?(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: oidc 로그인 * feat: TaskEntity name 추가 * feat: 배포 설정 * feat: 유저 정보에 학번 추가, 로그인 시 sub claim 확인 * feat: 배포 테스트 위해 redirect-uri 변경 * fix: groups claim 소문자로 수정 * feat: 예약 엔티티 및 DTO 설계 * feat: 일반 예약 및 정기 예약 --- .../wafflestudio/csereal/common/Exceptions.kt | 1 + .../reservation/api/ReservceController.kt | 53 ++++++++++++ .../reservation/database/ReservationEntity.kt | 69 ++++++++++++++++ .../database/ReservationRepository.kt | 15 ++++ .../core/reservation/database/RoomEntity.kt | 21 +++++ .../reservation/database/RoomRepository.kt | 6 ++ .../core/reservation/dto/ReservationDto.kt | 34 ++++++++ .../core/reservation/dto/ReserveRequest.kt | 14 ++++ .../reservation/service/ReservationService.kt | 81 +++++++++++++++++++ 9 files changed, 294 insertions(+) create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/reservation/api/ReservceController.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/ReservationEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/ReservationRepository.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/RoomEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/RoomRepository.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/reservation/dto/ReservationDto.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/reservation/dto/ReserveRequest.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/reservation/service/ReservationService.kt diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/Exceptions.kt b/src/main/kotlin/com/wafflestudio/csereal/common/Exceptions.kt index 2809d4f6..ead4693b 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/common/Exceptions.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/common/Exceptions.kt @@ -6,4 +6,5 @@ open class CserealException(msg: String, val status: HttpStatus) : RuntimeExcept class Csereal400(msg: String) : CserealException(msg, HttpStatus.BAD_REQUEST) class Csereal404(msg: String) : CserealException(msg, HttpStatus.NOT_FOUND) class Csereal401(msg: String) : CserealException(msg, HttpStatus.UNAUTHORIZED) + class Csereal409(msg: String) : CserealException(msg, HttpStatus.CONFLICT) } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/api/ReservceController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/api/ReservceController.kt new file mode 100644 index 00000000..82e9962a --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/api/ReservceController.kt @@ -0,0 +1,53 @@ +package com.wafflestudio.csereal.core.reservation.api + +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.reservation.dto.ReservationDto +import com.wafflestudio.csereal.core.reservation.dto.ReserveRequest +import com.wafflestudio.csereal.core.reservation.service.ReservationService +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.security.oauth2.core.oidc.user.OidcUser +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDateTime +import java.util.UUID + +@RequestMapping("/reservation") +@RestController +class ReservationController( + private val reservationService: ReservationService +) { + + @PostMapping + fun reserveRoom( + @AuthenticationPrincipal principal: OidcUser?, + @RequestBody reserveRequest: ReserveRequest + ): List { + if (principal == null) { + throw CserealException.Csereal401("로그인이 필요합니다.") + } + val username = principal.idToken.getClaim("username") + return reservationService.reserveRoom(username, reserveRequest) + } + + @DeleteMapping("/{reservationId}") + fun cancelSpecific(@AuthenticationPrincipal principal: OidcUser?, @PathVariable reservationId: Long) { + if (principal == null) { + throw CserealException.Csereal401("로그인이 필요합니다.") + } + reservationService.cancelSpecific(reservationId) + } + + @DeleteMapping("/recurring/{recurrenceId}") + fun cancelRecurring(@AuthenticationPrincipal principal: OidcUser?, @PathVariable recurrenceId: UUID) { + if (principal == null) { + throw CserealException.Csereal401("로그인이 필요합니다.") + } + reservationService.cancelRecurring(recurrenceId) + } + +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/ReservationEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/ReservationEntity.kt new file mode 100644 index 00000000..8412f229 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/ReservationEntity.kt @@ -0,0 +1,69 @@ +package com.wafflestudio.csereal.core.reservation.database + +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.core.reservation.dto.ReserveRequest +import com.wafflestudio.csereal.core.user.database.UserEntity +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.PrePersist +import jakarta.persistence.PreUpdate +import java.time.LocalDateTime +import java.util.* + +@Entity(name = "reservation") +class ReservationEntity( + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + val user: UserEntity, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id") + val room: RoomEntity, + + val title: String, + val contactEmail: String, + val contactPhone: String, + val purpose: String, + val startTime: LocalDateTime, + val endTime: LocalDateTime, + + val recurrenceId: UUID? = null + +) : BaseTimeEntity() { + + @PrePersist + @PreUpdate + fun validateDates() { + if (startTime.isAfter(endTime) || startTime.isEqual(endTime)) { + throw CserealException.Csereal400("종료 시각은 시작 시각 이후여야 합니다.") + } + } + + companion object { + fun create( + user: UserEntity, + room: RoomEntity, + reserveRequest: ReserveRequest, + start: LocalDateTime, + end: LocalDateTime, + recurrenceId: UUID + ): ReservationEntity { + return ReservationEntity( + user = user, + room = room, + title = reserveRequest.title, + contactEmail = reserveRequest.contactEmail, + contactPhone = reserveRequest.contactPhone, + purpose = reserveRequest.purpose, + startTime = start, + endTime = end, + recurrenceId = recurrenceId + ) + } + } + +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/ReservationRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/ReservationRepository.kt new file mode 100644 index 00000000..827db80d --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/ReservationRepository.kt @@ -0,0 +1,15 @@ +package com.wafflestudio.csereal.core.reservation.database + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import java.time.LocalDateTime +import java.util.UUID + +interface ReservationRepository : JpaRepository { + + @Query("SELECT r FROM reservation r WHERE r.room.id = :roomId AND ((:start <= r.startTime AND r.startTime < :end) OR (:start < r.endTime AND r.endTime <= :end) OR (r.startTime <= :start AND r.endTime >= :end))") + fun findByRoomIdAndTimeOverlap(roomId: Long, start: LocalDateTime, end: LocalDateTime): List + + fun deleteAllByRecurrenceId(recurrenceId: UUID) + +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/RoomEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/RoomEntity.kt new file mode 100644 index 00000000..f76d1c48 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/RoomEntity.kt @@ -0,0 +1,21 @@ +package com.wafflestudio.csereal.core.reservation.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated + +@Entity(name = "room") +class RoomEntity( + val name: String?, + val location: String, + + val capacity: Int, + + @Enumerated(EnumType.STRING) + val type: RoomType +) : BaseTimeEntity() + +enum class RoomType { + SEMINAR, LAB, LECTURE +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/RoomRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/RoomRepository.kt new file mode 100644 index 00000000..c7c9b562 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/RoomRepository.kt @@ -0,0 +1,6 @@ +package com.wafflestudio.csereal.core.reservation.database + +import org.springframework.data.jpa.repository.JpaRepository + +interface RoomRepository : JpaRepository { +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/dto/ReservationDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/dto/ReservationDto.kt new file mode 100644 index 00000000..b868d4b2 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/dto/ReservationDto.kt @@ -0,0 +1,34 @@ +package com.wafflestudio.csereal.core.reservation.dto + +import com.wafflestudio.csereal.core.reservation.database.ReservationEntity +import java.time.LocalDateTime + +data class ReservationDto( + val id: Long, + val title: String, + val purpose: String, + val startTime: LocalDateTime, + val endTime: LocalDateTime, + val roomName: String?, + val roomLocation: String, + val userName: String, + val contactEmail: String, + val contactPhone: String +) { + companion object { + fun of(reservationEntity: ReservationEntity): ReservationDto { + return ReservationDto( + id = reservationEntity.id, + title = reservationEntity.title, + purpose = reservationEntity.purpose, + startTime = reservationEntity.startTime, + endTime = reservationEntity.endTime, + roomName = reservationEntity.room.name, + roomLocation = reservationEntity.room.location, + userName = reservationEntity.user.username, + contactEmail = reservationEntity.contactEmail, + contactPhone = reservationEntity.contactPhone + ) + } + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/dto/ReserveRequest.kt b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/dto/ReserveRequest.kt new file mode 100644 index 00000000..55befd76 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/dto/ReserveRequest.kt @@ -0,0 +1,14 @@ +package com.wafflestudio.csereal.core.reservation.dto + +import java.time.LocalDateTime + +data class ReserveRequest( + val roomId: Long, + val title: String, + val contactEmail: String, + val contactPhone: String, + val purpose: String, + val startTime: LocalDateTime, + val endTime: LocalDateTime, + val recurringWeeks: Int? = null +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/service/ReservationService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/service/ReservationService.kt new file mode 100644 index 00000000..8adba80c --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/service/ReservationService.kt @@ -0,0 +1,81 @@ +package com.wafflestudio.csereal.core.reservation.service + +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.reservation.database.* +import com.wafflestudio.csereal.core.reservation.dto.ReservationDto +import com.wafflestudio.csereal.core.reservation.dto.ReserveRequest +import com.wafflestudio.csereal.core.user.database.Role +import com.wafflestudio.csereal.core.user.database.UserRepository +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.* + +interface ReservationService { + fun reserveRoom( + username: String, + reserveRequest: ReserveRequest + ): List + + fun cancelSpecific(reservationId: Long) + fun cancelRecurring(recurrenceId: UUID) +} + +@Service +@Transactional +class ReservationServiceImpl( + private val reservationRepository: ReservationRepository, + private val userRepository: UserRepository, + private val roomRepository: RoomRepository +) : ReservationService { + + override fun reserveRoom( + username: String, + reserveRequest: ReserveRequest + ): List { + val user = userRepository.findByUsername(username) ?: throw CserealException.Csereal404("재로그인이 필요합니다.") + val room = + roomRepository.findByIdOrNull(reserveRequest.roomId) ?: throw CserealException.Csereal404("Room Not Found") + + if (user.role == null) { + throw CserealException.Csereal401("권한이 없습니다.") + } + + val reservations = mutableListOf() + + val recurrenceId = UUID.randomUUID() + + val numberOfWeeks = reserveRequest.recurringWeeks ?: 1 + + for (week in 0 until numberOfWeeks) { + val start = reserveRequest.startTime.plusWeeks(week.toLong()) + val end = reserveRequest.endTime.plusWeeks(week.toLong()) + + // 중복 예약 방지 + val overlappingReservations = reservationRepository.findByRoomIdAndTimeOverlap( + reserveRequest.roomId, + start, + end + ) + if (overlappingReservations.isNotEmpty()) { + throw CserealException.Csereal409("${week}주차 해당 시간에 이미 예약이 있습니다.") + } + + val newReservation = ReservationEntity.create(user, room, reserveRequest, start, end, recurrenceId) + reservations.add(newReservation) + } + + reservationRepository.saveAll(reservations) + + return reservations.map { ReservationDto.of(it) } + } + + override fun cancelSpecific(reservationId: Long) { + reservationRepository.deleteById(reservationId) + } + + override fun cancelRecurring(recurrenceId: UUID) { + reservationRepository.deleteAllByRecurrenceId(recurrenceId) + } + +}