From 04f67eba052133c1ae200625f8950602663e09cc Mon Sep 17 00:00:00 2001 From: Hanna Date: Wed, 10 Jan 2024 10:32:52 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20seat=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EB=A9=A4=EB=B2=84=20=EC=97=B0=EB=8F=99=20=EB=B0=8F=20booking?= =?UTF-8?q?=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/exception/BookingException.java | 6 ++--- .../exception/BookingExceptionHandler.java | 13 ++++----- .../booking/controller/BookingController.java | 6 +++-- .../booking/service/BookingService.java | 17 +++++------- .../seat/controller/SeatController.java | 9 ++++--- .../domain/seat/service/SeatLockService.java | 14 ++++++++++ .../domain/seat/service/SeatService.java | 27 ++++++++++++------- .../domain/common/BookingErrorCode.java | 2 ++ 8 files changed, 57 insertions(+), 37 deletions(-) diff --git a/api/api-booking/src/main/java/com/pgms/apibooking/common/exception/BookingException.java b/api/api-booking/src/main/java/com/pgms/apibooking/common/exception/BookingException.java index 7114b266..16eac5d6 100644 --- a/api/api-booking/src/main/java/com/pgms/apibooking/common/exception/BookingException.java +++ b/api/api-booking/src/main/java/com/pgms/apibooking/common/exception/BookingException.java @@ -1,15 +1,15 @@ package com.pgms.apibooking.common.exception; -import com.pgms.coredomain.domain.common.BookingErrorCode; +import com.pgms.coredomain.domain.common.BaseErrorCode; import lombok.Getter; @Getter public class BookingException extends RuntimeException { - private final BookingErrorCode errorCode; + private final BaseErrorCode errorCode; - public BookingException(BookingErrorCode errorCode) { + public BookingException(BaseErrorCode errorCode) { super(errorCode.getMessage()); this.errorCode = errorCode; } diff --git a/api/api-booking/src/main/java/com/pgms/apibooking/common/exception/BookingExceptionHandler.java b/api/api-booking/src/main/java/com/pgms/apibooking/common/exception/BookingExceptionHandler.java index c9df8a07..785f6ad0 100644 --- a/api/api-booking/src/main/java/com/pgms/apibooking/common/exception/BookingExceptionHandler.java +++ b/api/api-booking/src/main/java/com/pgms/apibooking/common/exception/BookingExceptionHandler.java @@ -28,8 +28,7 @@ public class BookingExceptionHandler extends ResponseEntityExceptionHandler { protected ResponseEntity handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) { if (ex instanceof BindException) { - BookingErrorCode errorCode = BookingErrorCode.INVALID_INPUT_VALUE; - ErrorResponse response = new ErrorResponse(errorCode.getCode(), errorCode.getMessage()); + ErrorResponse response = BookingErrorCode.INVALID_INPUT_VALUE.getErrorResponse(); ((BindException)ex).getBindingResult().getAllErrors().forEach(e -> { String fieldName = ((FieldError)e).getField(); @@ -42,16 +41,14 @@ protected ResponseEntity handleExceptionInternal(Exception ex, Object bo log.error(ex.getMessage(), ex); - BookingErrorCode errorCode = BookingErrorCode.INTERNAL_SERVER_ERROR; - ErrorResponse response = new ErrorResponse(errorCode.getCode(), errorCode.getMessage()); + ErrorResponse response = BookingErrorCode.INTERNAL_SERVER_ERROR.getErrorResponse(); return ResponseEntity.internalServerError().body(response); } @Override protected ResponseEntity handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { - BookingErrorCode errorCode = BookingErrorCode.INVALID_INPUT_VALUE; - ErrorResponse response = new ErrorResponse(errorCode.getCode(), errorCode.getMessage()); + ErrorResponse response = BookingErrorCode.INVALID_INPUT_VALUE.getErrorResponse(); return ResponseEntity.badRequest().body(response); } @@ -61,13 +58,13 @@ protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotV BindingResult bindingResult = ex.getBindingResult(); String errorMessage = Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage(); log.warn("Validation Failed: {}", errorMessage); - ErrorResponse response = new ErrorResponse(BookingErrorCode.INVALID_INPUT_VALUE.getCode(), errorMessage); + ErrorResponse response = BookingErrorCode.INVALID_INPUT_VALUE.getErrorResponse(); return ResponseEntity.status(status).body(response); } @ExceptionHandler(BookingException.class) protected ResponseEntity handleBookingException(BookingException ex) { - ErrorResponse response = new ErrorResponse(ex.getErrorCode().getCode(), ex.getErrorCode().getMessage()); + ErrorResponse response = ex.getErrorCode().getErrorResponse(); log.warn("Booking Exception Occurred : {}", response.getErrorMessage()); return ResponseEntity.status(ex.getErrorCode().getStatus()).body(response); } diff --git a/api/api-booking/src/main/java/com/pgms/apibooking/domain/booking/controller/BookingController.java b/api/api-booking/src/main/java/com/pgms/apibooking/domain/booking/controller/BookingController.java index 1e7e4697..20aa1687 100644 --- a/api/api-booking/src/main/java/com/pgms/apibooking/domain/booking/controller/BookingController.java +++ b/api/api-booking/src/main/java/com/pgms/apibooking/domain/booking/controller/BookingController.java @@ -38,7 +38,8 @@ public class BookingController { public ResponseEntity> createBooking( //@CurrentAccount Long memberId, @RequestBody @Valid BookingCreateRequest request, - HttpServletRequest httpRequest) { + HttpServletRequest httpRequest + ) { BookingCreateResponse createdBooking = bookingService.createBooking(request, 1L); //TODO: 인증된 memberId 지정 ApiResponse response = ApiResponse.ok(createdBooking); URI location = UriComponentsBuilder @@ -53,7 +54,8 @@ public ResponseEntity> createBooking( public ResponseEntity cancelBooking( //@CurrentAccount Long memberId, @PathVariable String id, - @RequestBody @Valid BookingCancelRequest request) { + @RequestBody @Valid BookingCancelRequest request + ) { bookingService.cancelBooking(id, request, 1L); //TODO: 인증된 memberId 지정 return ResponseEntity.ok().build(); } diff --git a/api/api-booking/src/main/java/com/pgms/apibooking/domain/booking/service/BookingService.java b/api/api-booking/src/main/java/com/pgms/apibooking/domain/booking/service/BookingService.java index 5e845c19..3593210c 100644 --- a/api/api-booking/src/main/java/com/pgms/apibooking/domain/booking/service/BookingService.java +++ b/api/api-booking/src/main/java/com/pgms/apibooking/domain/booking/service/BookingService.java @@ -1,7 +1,6 @@ package com.pgms.apibooking.domain.booking.service; import java.util.List; -import java.util.NoSuchElementException; import java.util.Optional; import org.springframework.data.domain.PageRequest; @@ -13,9 +12,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import com.pgms.coredomain.domain.common.BookingErrorCode; import com.pgms.apibooking.common.exception.BookingException; -import com.pgms.coresecurity.security.jwt.booking.BookingAuthToken; import com.pgms.apibooking.config.TossPaymentConfig; import com.pgms.apibooking.domain.booking.dto.request.BookingCancelRequest; import com.pgms.apibooking.domain.booking.dto.request.BookingCreateRequest; @@ -39,6 +36,8 @@ import com.pgms.coredomain.domain.booking.Ticket; import com.pgms.coredomain.domain.booking.repository.BookingRepository; import com.pgms.coredomain.domain.booking.repository.TicketRepository; +import com.pgms.coredomain.domain.common.BookingErrorCode; +import com.pgms.coredomain.domain.common.MemberErrorCode; import com.pgms.coredomain.domain.event.EventSeat; import com.pgms.coredomain.domain.event.EventSeatStatus; import com.pgms.coredomain.domain.event.EventTime; @@ -46,6 +45,7 @@ import com.pgms.coredomain.domain.event.repository.EventTimeRepository; import com.pgms.coredomain.domain.member.Member; import com.pgms.coredomain.domain.member.repository.MemberRepository; +import com.pgms.coresecurity.security.jwt.booking.BookingAuthToken; import lombok.RequiredArgsConstructor; @@ -138,10 +138,8 @@ public PageResponse getBookings( BookingSearchCondition searchCondition, Long memberId ) { - Member member = getMemberById(memberId); - Pageable pageable = PageRequest.of(pageCondition.getPage() - 1, pageCondition.getSize()); - searchCondition.updateMemberId(member.getId()); + searchCondition.updateMemberId(memberId); List bookings = bookingQuerydslRepository.findAll(searchCondition, pageable) .stream() @@ -155,12 +153,10 @@ public PageResponse getBookings( @Transactional(readOnly = true) public BookingGetResponse getBooking(String id, Long memberId) { - Member member = getMemberById(memberId); - Booking booking = bookingRepository.findBookingInfoById(id) .orElseThrow(() -> new BookingException(BookingErrorCode.BOOKING_NOT_FOUND)); - if (!booking.isSameBooker(member.getId())) { + if (!booking.isSameBooker(memberId)) { throw new BookingException(BookingErrorCode.FORBIDDEN); } @@ -213,9 +209,8 @@ private void validateRefundReceiveAccount(PaymentMethod paymentMethod, } private Member getMemberById(Long memberId) { - System.out.println("member id get " + memberId); return memberRepository.findById(memberId) - .orElseThrow(() -> new NoSuchElementException("Member not found")); + .orElseThrow(() -> new BookingException(MemberErrorCode.MEMBER_NOT_FOUND)); } private String getCurrentSessionId() { diff --git a/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/controller/SeatController.java b/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/controller/SeatController.java index 602e939d..5f7b2e65 100644 --- a/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/controller/SeatController.java +++ b/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/controller/SeatController.java @@ -14,6 +14,7 @@ import com.pgms.apibooking.domain.seat.dto.response.AreaResponse; import com.pgms.apibooking.domain.seat.service.SeatService; import com.pgms.coredomain.response.ApiResponse; +import com.pgms.coresecurity.security.resolver.CurrentAccount; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -32,14 +33,14 @@ public ResponseEntity>> getSeats(@ModelAttribute } @PostMapping("/{seatId}/select") - public ResponseEntity selectSeat(@PathVariable Long seatId) { - seatService.selectSeat(seatId); + public ResponseEntity selectSeat(@PathVariable Long seatId, @CurrentAccount Long memberId) { + seatService.selectSeat(seatId, memberId); return ResponseEntity.ok().build(); } @PostMapping("/{seatId}/deselect") - public ResponseEntity deselectSeat(@PathVariable Long seatId) { - seatService.deselectSeat(seatId); + public ResponseEntity deselectSeat(@PathVariable Long seatId, @CurrentAccount Long memberId) { + seatService.deselectSeat(seatId, memberId); return ResponseEntity.ok().build(); } } diff --git a/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/service/SeatLockService.java b/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/service/SeatLockService.java index 9d9f22b3..5dd3b5d8 100644 --- a/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/service/SeatLockService.java +++ b/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/service/SeatLockService.java @@ -1,6 +1,7 @@ package com.pgms.apibooking.domain.seat.service; import java.time.Duration; +import java.util.Optional; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; @@ -17,6 +18,15 @@ class SeatLockService { //TODO: 레디스 레포지토리 분리 private final RedisTemplate redisTemplate; + Optional getSelectorId(Long seatId) { + String key = generateSeatLockKey(seatId); + String value = redisTemplate.opsForValue().get(key); + if (value == null) { + return Optional.empty(); + } + return Optional.of(extractMemberId(value)); + } + boolean isSeatLocked(Long seatId) { return redisTemplate.opsForValue().get(generateSeatLockKey(seatId)) != null; } @@ -39,4 +49,8 @@ private String generateSeatLockKey(Long seatId) { private String generateSeatLockValue(Long memberId) { return SEAT_LOCK_CACHE_VALUE_PREFIX + memberId; } + + private Long extractMemberId(String value) { + return Long.parseLong(value.replace(SEAT_LOCK_CACHE_VALUE_PREFIX, "")); + } } diff --git a/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/service/SeatService.java b/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/service/SeatService.java index 525cd90e..a91a3dd5 100644 --- a/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/service/SeatService.java +++ b/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/service/SeatService.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import org.springframework.stereotype.Service; @@ -37,9 +38,7 @@ public List getSeats(SeatsGetRequest request) { .toList(); } - public void selectSeat(Long seatId) { - Long memberId = 0L; //TODO: 인증된 memberId 지정 - + public void selectSeat(Long seatId, Long memberId) { if (seatLockService.isSeatLocked(seatId)) { throw new BookingException(BookingErrorCode.SEAT_BEING_BOOKED); } @@ -54,14 +53,19 @@ public void selectSeat(Long seatId) { seatLockService.lockSeat(seatId, memberId); } - public void deselectSeat(Long seatId) { - Long memberId = 0L; //TODO: 인증된 memberId 지정 - - //TODO: lock 걸어둔 멤버에게 요청이 들어왔는지 검증 후, 아래 로직을 수행한다 + public void deselectSeat(Long seatId, Long memberId) { + Optional selectorIdOpt = seatLockService.getSelectorId(seatId); - EventSeat seat = getSeat(seatId); + if(selectorIdOpt.isEmpty()) { + updateSeatStatusToAvailable(seatId); + throw new BookingException(BookingErrorCode.SEAT_SELECTION_EXPIRED); + } + + if (!selectorIdOpt.get().equals(memberId)) { + throw new BookingException(BookingErrorCode.SEAT_SELECTED_BY_ANOTHER_MEMBER); + } - seat.updateStatus(EventSeatStatus.AVAILABLE); + updateSeatStatusToAvailable(seatId); seatLockService.unlockSeat(seatId); } @@ -69,4 +73,9 @@ private EventSeat getSeat(Long seatId) { return eventSeatRepository.findById(seatId) .orElseThrow(() -> new BookingException(BookingErrorCode.SEAT_NOT_FOUND)); } + + private void updateSeatStatusToAvailable(Long seatId) { + EventSeat seat = getSeat(seatId); + seat.updateStatus(EventSeatStatus.AVAILABLE); + } } diff --git a/core/core-domain/src/main/java/com/pgms/coredomain/domain/common/BookingErrorCode.java b/core/core-domain/src/main/java/com/pgms/coredomain/domain/common/BookingErrorCode.java index 271a1b78..ca4168f2 100644 --- a/core/core-domain/src/main/java/com/pgms/coredomain/domain/common/BookingErrorCode.java +++ b/core/core-domain/src/main/java/com/pgms/coredomain/domain/common/BookingErrorCode.java @@ -13,6 +13,8 @@ public enum BookingErrorCode implements BaseErrorCode{ SEAT_NOT_FOUND(HttpStatus.BAD_REQUEST, "SEAT_NOT_FOUND", "존재하지 않는 좌석입니다."), SEAT_BEING_BOOKED(HttpStatus.BAD_REQUEST, "SEAT_BEING_BOOKED", "예매중인 좌석입니다."), SEAT_ALREADY_BOOKED(HttpStatus.BAD_REQUEST, "SEAT_ALREADY_BOOKED", "예매된 좌석입니다."), + SEAT_SELECTED_BY_ANOTHER_MEMBER(HttpStatus.BAD_REQUEST, "SEAT_SELECTED_BY_ANOTHER_MEMBER", "다른 회원이 선택한 좌석입니다."), + SEAT_SELECTION_EXPIRED(HttpStatus.BAD_REQUEST, "SEAT_SELECTION_EXPIRED", "좌석 선택 시간이 만료되었습니다."), TIME_NOT_FOUND(HttpStatus.BAD_REQUEST, "TIME_NOT_FOUND", "존재하지 않는 공연 회차입니다."), UNBOOKABLE_EVENT(HttpStatus.BAD_REQUEST, "UNBOOKABLE_EVENT", "현재 예매가 불가능한 공연입니다."),