package roomescape.reservation.controller; import java.time.LocalDate; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; 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.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.headers.Header; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import roomescape.auth.web.support.Admin; import roomescape.auth.web.support.LoginRequired; import roomescape.auth.web.support.MemberId; import roomescape.common.dto.response.RoomescapeApiResponse; import roomescape.common.dto.response.RoomescapeErrorResponse; import roomescape.common.exception.RoomescapeException; import roomescape.payment.client.TossPaymentClient; import roomescape.payment.dto.request.PaymentCancelRequest; import roomescape.payment.dto.request.PaymentRequest; import roomescape.payment.dto.response.PaymentCancelResponse; import roomescape.payment.dto.response.PaymentResponse; import roomescape.reservation.dto.request.AdminReservationRequest; import roomescape.reservation.dto.request.ReservationRequest; import roomescape.reservation.dto.request.WaitingRequest; import roomescape.reservation.dto.response.MyReservationsResponse; import roomescape.reservation.dto.response.ReservationResponse; import roomescape.reservation.dto.response.ReservationsResponse; import roomescape.reservation.service.ReservationService; import roomescape.reservation.service.ReservationWithPaymentService; @RestController @Tag(name = "3. 예약 API", description = "예약 및 대기 정보를 추가 / 조회 / 삭제할 때 사용합니다.") public class ReservationController { private final ReservationWithPaymentService reservationWithPaymentService; private final ReservationService reservationService; private final TossPaymentClient paymentClient; public ReservationController(ReservationWithPaymentService reservationWithPaymentService, ReservationService reservationService, TossPaymentClient paymentClient) { this.reservationWithPaymentService = reservationWithPaymentService; this.reservationService = reservationService; this.paymentClient = paymentClient; } @Admin @GetMapping("/reservations") @ResponseStatus(HttpStatus.OK) @Operation(summary = "모든 예약 정보 조회", tags = "관리자 로그인이 필요한 API") @ApiResponses({ @ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true) }) public RoomescapeApiResponse getAllReservations() { return RoomescapeApiResponse.success(reservationService.findAllReservations()); } @LoginRequired @GetMapping("/reservations-mine") @ResponseStatus(HttpStatus.OK) @Operation(summary = "자신의 예약 및 대기 조회", tags = "로그인이 필요한 API") @ApiResponses({ @ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true) }) public RoomescapeApiResponse getMemberReservations( @MemberId @Parameter(hidden = true) Long memberId) { return RoomescapeApiResponse.success(reservationService.findMemberReservations(memberId)); } @Admin @GetMapping("/reservations/search") @ResponseStatus(HttpStatus.OK) @Operation(summary = "관리자의 예약 검색", description = "특정 조건에 해당되는 예약 검색", tags = "관리자 로그인이 필요한 API") @ApiResponses({ @ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true), @ApiResponse(responseCode = "400", description = "날짜 범위를 지정할 때, 종료 날짜는 시작 날짜 이전일 수 없습니다.", content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class))) }) public RoomescapeApiResponse getReservationBySearching( @RequestParam(required = false) @Parameter(description = "테마 ID") Long themeId, @RequestParam(required = false) @Parameter(description = "회원 ID") Long memberId, @RequestParam(required = false) @Parameter(description = "yyyy-MM-dd 형식으로 입력해주세요", example = "2024-06-10") LocalDate dateFrom, @RequestParam(required = false) @Parameter(description = "yyyy-MM-dd 형식으로 입력해주세요", example = "2024-06-10") LocalDate dateTo ) { return RoomescapeApiResponse.success( reservationService.findFilteredReservations(themeId, memberId, dateFrom, dateTo)); } @Admin @DeleteMapping("/reservations/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) @Operation(summary = "관리자의 예약 취소", tags = "관리자 로그인이 필요한 API") @ApiResponses({ @ApiResponse(responseCode = "204", description = "성공"), @ApiResponse(responseCode = "404", description = "예약 또는 결제 정보를 찾을 수 없습니다.", content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class))), }) public RoomescapeApiResponse removeReservation( @MemberId @Parameter(hidden = true) Long memberId, @NotNull(message = "reservationId는 null일 수 없습니다.") @PathVariable("id") @Parameter(description = "예약 ID") Long reservationId ) { if (reservationWithPaymentService.isNotPaidReservation(reservationId)) { reservationService.removeReservationById(reservationId, memberId); return RoomescapeApiResponse.success(); } PaymentCancelRequest paymentCancelRequest = reservationWithPaymentService.removeReservationWithPayment( reservationId, memberId); PaymentCancelResponse paymentCancelResponse = paymentClient.cancelPayment(paymentCancelRequest); reservationWithPaymentService.updateCanceledTime(paymentCancelRequest.paymentKey(), paymentCancelResponse.canceledAt()); return RoomescapeApiResponse.success(); } @LoginRequired @PostMapping("/reservations") @ResponseStatus(HttpStatus.CREATED) @Operation(summary = "예약 추가", tags = "로그인이 필요한 API") @ApiResponses({ @ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true, headers = @Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = @Schema(example = "/reservations/1"))) }) public RoomescapeApiResponse saveReservation( @Valid @RequestBody ReservationRequest reservationRequest, @MemberId @Parameter(hidden = true) Long memberId, HttpServletResponse response ) { PaymentRequest paymentRequest = reservationRequest.getPaymentRequest(); PaymentResponse paymentResponse = paymentClient.confirmPayment(paymentRequest); try { ReservationResponse reservationResponse = reservationWithPaymentService.addReservationWithPayment( reservationRequest, paymentResponse, memberId); return getCreatedReservationResponse(reservationResponse, response); } catch (RoomescapeException e) { PaymentCancelRequest cancelRequest = new PaymentCancelRequest(paymentRequest.paymentKey(), paymentRequest.amount(), e.getMessage()); PaymentCancelResponse paymentCancelResponse = paymentClient.cancelPayment(cancelRequest); reservationWithPaymentService.saveCanceledPayment(paymentCancelResponse, paymentResponse.approvedAt(), paymentRequest.paymentKey()); throw e; } } @Admin @PostMapping("/reservations/admin") @ResponseStatus(HttpStatus.CREATED) @Operation(summary = "관리자 예약 추가", tags = "관리자 로그인이 필요한 API") @ApiResponses({ @ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true, headers = @Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = @Schema(example = "/reservations/1"))), @ApiResponse(responseCode = "409", description = "예약이 이미 존재합니다.", content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class))) }) public RoomescapeApiResponse saveReservationByAdmin( @Valid @RequestBody AdminReservationRequest adminReservationRequest, HttpServletResponse response ) { ReservationResponse reservationResponse = reservationService.addReservationByAdmin(adminReservationRequest); return getCreatedReservationResponse(reservationResponse, response); } @Admin @GetMapping("/reservations/waiting") @ResponseStatus(HttpStatus.OK) @Operation(summary = "모든 예약 대기 조회", tags = "관리자 로그인이 필요한 API") @ApiResponses({ @ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true) }) public RoomescapeApiResponse getAllWaiting() { return RoomescapeApiResponse.success(reservationService.findAllWaiting()); } @LoginRequired @PostMapping("/reservations/waiting") @ResponseStatus(HttpStatus.CREATED) @Operation(summary = "예약 대기 신청", tags = "로그인이 필요한 API") @ApiResponses({ @ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true, headers = @Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = @Schema(example = "/reservations/1"))) }) public RoomescapeApiResponse saveWaiting( @Valid @RequestBody WaitingRequest waitingRequest, @MemberId @Parameter(hidden = true) Long memberId, HttpServletResponse response ) { ReservationResponse reservationResponse = reservationService.addWaiting(waitingRequest, memberId); return getCreatedReservationResponse(reservationResponse, response); } @LoginRequired @DeleteMapping("/reservations/waiting/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) @Operation(summary = "예약 대기 취소", tags = "로그인이 필요한 API") @ApiResponses({ @ApiResponse(responseCode = "204", description = "성공"), @ApiResponse(responseCode = "404", description = "회원의 예약 대기 정보를 찾을 수 없습니다.", content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class))) }) public RoomescapeApiResponse deleteWaiting( @MemberId @Parameter(hidden = true) Long memberId, @NotNull(message = "reservationId는 null 또는 공백일 수 없습니다.") @PathVariable("id") @Parameter(description = "예약 ID") Long reservationId ) { reservationService.cancelWaiting(reservationId, memberId); return RoomescapeApiResponse.success(); } @Admin @PostMapping("/reservations/waiting/{id}/approve") @ResponseStatus(HttpStatus.OK) @Operation(summary = "대기 중인 예약 승인", tags = "관리자 로그인이 필요한 API") @ApiResponses({ @ApiResponse(responseCode = "200", description = "성공"), @ApiResponse(responseCode = "404", description = "예약 대기 정보를 찾을 수 없습니다.", content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class))), @ApiResponse(responseCode = "409", description = "확정된 예약이 존재하여 대기 중인 예약을 승인할 수 없습니다.", content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class))) }) public RoomescapeApiResponse approveWaiting( @MemberId @Parameter(hidden = true) Long memberId, @NotNull(message = "reservationId는 null 또는 공백일 수 없습니다.") @PathVariable("id") @Parameter(description = "예약 ID") Long reservationId ) { reservationService.approveWaiting(reservationId, memberId); return RoomescapeApiResponse.success(); } @Admin @PostMapping("/reservations/waiting/{id}/deny") @ResponseStatus(HttpStatus.NO_CONTENT) @Operation(summary = "대기 중인 예약 거절", tags = "관리자 로그인이 필요한 API") @ApiResponses({ @ApiResponse(responseCode = "204", description = "대기 중인 예약 거절 성공"), @ApiResponse(responseCode = "404", description = "예약 대기 정보를 찾을 수 없습니다.", content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class))) }) public RoomescapeApiResponse denyWaiting( @MemberId @Parameter(hidden = true) Long memberId, @NotNull(message = "reservationId는 null 또는 공백일 수 없습니다.") @PathVariable("id") @Parameter(description = "예약 ID") Long reservationId ) { reservationService.denyWaiting(reservationId, memberId); return RoomescapeApiResponse.success(); } private RoomescapeApiResponse getCreatedReservationResponse( ReservationResponse reservationResponse, HttpServletResponse response ) { response.setHeader(HttpHeaders.LOCATION, "/reservations/" + reservationResponse.id()); return RoomescapeApiResponse.success(reservationResponse); } }