Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 예매 생성 시 내가 선점한 좌석인지 검증하는 로직 추가 및 예매 토큰 인터셉터 버그 수정 #228

Merged
merged 13 commits into from
Jan 11, 2024
Merged
60 changes: 26 additions & 34 deletions api/api-booking/http/booking.http
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
### 세션 아이디 발급
POST http://localhost:8082/api/v1/bookings/issue-session-id
Authorization: Bearer {{token}}
Authorization: Bearer {{accessToken}}

### 대기열 진입
POST http://localhost:8082/api/v1/bookings/enter-queue
Content-Type: application/json
Authorization: Bearer {{token}}
Booking-Session-Id: {sessionId}
Authorization: Bearer {{accessToken}}
Booking-Session-Id: {{sessionId}}

{
"eventId": 1
}

### 대기열 조회
GET http://localhost:8082/api/v1/bookings/order-in-queue?eventId=1
Authorization: Bearer {{token}}
Booking-Session-Id: {sessionId}
Authorization: Bearer {{accessToken}}
Booking-Session-Id: {{sessionId}}


### 예매 토큰 발급
POST http://localhost:8082/api/v1/bookings/issue-token
Content-Type: application/json
Authorization: Bearer {{token}}
Booking-Session-Id: {sessionId}
Authorization: Bearer {{accessToken}}
Booking-Session-Id: {{sessionId}}

{
"eventId": 1
Expand All @@ -31,59 +31,51 @@ Booking-Session-Id: {sessionId}
### [Optional] 대기열 이탈
POST http://localhost:8082/api/v1/bookings/exit-queue
Content-Type: application/json
Authorization: Bearer {{token}}
Booking-Session-Id: {sessionId}
Authorization: Bearer {{accessToken}}
Booking-Session-Id: {{sessionId}}

{
"eventId": 1
}

### 좌석 목록 조회
GET http://localhost:8082/api/v1/seats?eventTimeId=1
Authorization: Bearer {{token}}
Booking-Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJib29raW5nIiwiaWF0IjoxNzA0NzA0NTY0LCJleHAiOjQyMDAxNzA0NzA0NTY0LCJzZXNzaW9uSWQiOiJ7Pz8_Pz99In0.iKZaud5vfvsMzXmQzl1WaCweL9GL00U0iGzCg4_p3Zzei8Y7z19Ff_TTpxh9gLeB
Authorization: Bearer {{accessToken}}
Booking-Authorization: Bearer {{bookingToken}}

### 좌석 선택
POST http://localhost:8082/api/v1/seats/1/select
Content-Type: application/json
Authorization: Bearer {{token}}
Booking-Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJib29raW5nIiwiaWF0IjoxNzA0NzA0NTY0LCJleHAiOjQyMDAxNzA0NzA0NTY0LCJzZXNzaW9uSWQiOiJ7Pz8_Pz99In0.iKZaud5vfvsMzXmQzl1WaCweL9GL00U0iGzCg4_p3Zzei8Y7z19Ff_TTpxh9gLeB

{
"seatId": 1
}
Authorization: Bearer {{accessToken}}
Booking-Authorization: Bearer {{bookingToken}}

### 좌석 선택 해제
POST http://localhost:8082/api/v1/seats/1/deselect
Content-Type: application/json
Authorization: Bearer {{token}}
Booking-Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJib29raW5nIiwiaWF0IjoxNzA0NzA0NTY0LCJleHAiOjQyMDAxNzA0NzA0NTY0LCJzZXNzaW9uSWQiOiJ7Pz8_Pz99In0.iKZaud5vfvsMzXmQzl1WaCweL9GL00U0iGzCg4_p3Zzei8Y7z19Ff_TTpxh9gLeB

{
"seatId": 1
}
Authorization: Bearer {{accessToken}}
Booking-Authorization: Bearer {{bookingToken}}

### 예매 ~ 결제 (브라우저에서 진행해 주세요)
### 예매 ~ 결제 승인 (브라우저에서 진행해 주세요)
GET http://localhost:8082/bookings

### [Optional] 예매 이탈
POST http://localhost:8082/api/v1/bookings/bookingCreateTestId/exit
Authorization: Bearer {{token}}
Booking-Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJib29raW5nIiwiaWF0IjoxNzA0NzA0NTY0LCJleHAiOjQyMDAxNzA0NzA0NTY0LCJzZXNzaW9uSWQiOiJ7Pz8_Pz99In0.iKZaud5vfvsMzXmQzl1WaCweL9GL00U0iGzCg4_p3Zzei8Y7z19Ff_TTpxh9gLeB
POST http://localhost:8082/api/v1/bookings/1/exit
Authorization: Bearer {{accessToken}}
Booking-Authorization: Bearer {{bookingToken}}

### 예매 취소 (브라우저에서 생성된 예매번호로 진행해 주세요)
POST http://localhost:8082/api/v1/bookings/{bookingId}/cancel
Authorization: Bearer {{token}}
### 예매 취소 (브라우저에서 생성된 예매 번호로 진행해 주세요)
POST http://localhost:8082/api/v1/bookings/1/cancel
Content-Type: application/json
Authorization: Bearer {{accessToken}}

{
"cancelReason": "변심"
"cancelReason": "구매자 변심"
}

### 내 예매 내역 목록 조회
GET http://localhost:8082/api/v1/bookings
Authorization: Bearer {{token}}
Authorization: Bearer {{accessToken}}

### 내 예매 내역 상세 조회
GET http://localhost:8082/api/v1/bookings/bookingCancelTestId
Authorization: Bearer {{token}}
GET http://localhost:8082/api/v1/bookings/{bookingId}
Authorization: Bearer {{accessToken}}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public void addInterceptors(InterceptorRegistry registry) {
.addPathPatterns("/api/*/bookings/create")
.addPathPatterns("/api/*/exit")
.addPathPatterns("/api/*/seats")
.addPathPatterns("/api/*/*/select")
.addPathPatterns("/api/*/*/deselect");
.addPathPatterns("/api/*/seats/*/select")
.addPathPatterns("/api/*/seats/*/deselect");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,13 @@ public ResponseEntity<Void> cancelBooking(
@PathVariable String id,
@RequestBody @Valid BookingCancelRequest request) {
bookingService.cancelBooking(id, request, memberId);
return ResponseEntity.ok().build();
return ResponseEntity.noContent().build();
}

@PostMapping("/{id}/exit")
public ResponseEntity<Void> exitBooking(@PathVariable String id) {
bookingService.exitBooking(id);
return ResponseEntity.ok().build();
return ResponseEntity.noContent().build();
}

@GetMapping
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.pgms.apibooking.domain.payment.dto.request.PaymentCancelRequest;
import com.pgms.apibooking.domain.payment.dto.request.RefundAccountRequest;
import com.pgms.apibooking.domain.payment.service.PaymentService;
import com.pgms.apibooking.domain.seat.service.SeatLockService;
import com.pgms.coredomain.domain.booking.Booking;
import com.pgms.coredomain.domain.booking.Payment;
import com.pgms.coredomain.domain.booking.PaymentMethod;
Expand Down Expand Up @@ -59,13 +60,14 @@ public class BookingService { //TODO: 테스트 코드 작성
private final MemberRepository memberRepository;
private final BookingQuerydslRepository bookingQuerydslRepository;
private final BookingQueueRepository bookingQueueRepository;
private final TossPaymentConfig tossPaymentConfig;
private final SeatLockService seatLockService;
private final PaymentService paymentService;
private final TossPaymentConfig tossPaymentConfig;

public BookingCreateResponse createBooking(BookingCreateRequest request, Long memberId, String tokenSessionId) {
Member member = getMemberById(memberId);
EventTime time = getBookableTimeWithEvent(request.timeId());
List<EventSeat> seats = getBookableSeatsWithArea(request.timeId(), request.seatIds());
List<EventSeat> seats = getBookableSeatsWithArea(request.timeId(), request.seatIds(), memberId);

ReceiptType receiptType = ReceiptType.fromDescription(request.receiptType());
validateDeliveryAddress(receiptType, request.deliveryAddress());
Expand Down Expand Up @@ -178,7 +180,14 @@ private EventTime getBookableTimeWithEvent(Long timeId) {
return time;
}

private List<EventSeat> getBookableSeatsWithArea(Long timeId, List<Long> seatIds) {
private List<EventSeat> getBookableSeatsWithArea(Long timeId, List<Long> seatIds, Long memberId) {
seatIds.forEach(seatId -> {
Long selectorId = seatLockService.getSelectorId(seatId);
if (selectorId == null || !selectorId.equals(memberId)) {
throw new BookingException(BookingErrorCode.UNBOOKABLE_SEAT_INCLUSION);
}
});

List<EventSeat> seats = eventSeatRepository.findAllWithAreaByTimeIdAndSeatIds(timeId, seatIds);

if (seats.size() != seatIds.size()) {
Expand All @@ -203,7 +212,7 @@ private void validateDeliveryAddress(ReceiptType receiptType, Optional<DeliveryA
private void validateRefundReceiveAccount(PaymentMethod paymentMethod, PaymentStatus paymentStatus,
Optional<RefundAccountRequest> refundReceiveAccount) {
if (paymentMethod == PaymentMethod.VIRTUAL_ACCOUNT && paymentStatus == PaymentStatus.DONE
&&refundReceiveAccount.isEmpty()){
&& refundReceiveAccount.isEmpty()) {
throw new BookingException(BookingErrorCode.REFUND_ACCOUNT_REQUIRED);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public ResponseEntity<ApiResponse<SessionIdIssueResponse>> issueSessionId() {
@PostMapping("/enter-queue")
public ResponseEntity<Void> enterQueue(@RequestBody @Valid BookingQueueEnterRequest request, @RequestAttribute("bookingSessionId") String bookingSessionId) {
bookingQueueService.enterQueue(request, bookingSessionId);
return ResponseEntity.ok().build();
return ResponseEntity.noContent().build();
}

@GetMapping("/order-in-queue")
Expand All @@ -56,6 +56,6 @@ public ResponseEntity<ApiResponse<TokenIssueResponse>> issueToken(@RequestBody @
@PostMapping("/exit-queue")
public ResponseEntity<Void> exitQueue(@RequestBody @Valid BookingQueueExitRequest request, @RequestAttribute("bookingSessionId") String bookingSessionId) {
bookingQueueService.exitQueue(request, bookingSessionId);
return ResponseEntity.ok().build();
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ public ResponseEntity<ApiResponse<List<AreaResponse>>> getSeats(@ModelAttribute
@PostMapping("/{seatId}/select")
public ResponseEntity<Void> selectSeat(@PathVariable Long seatId, @CurrentAccount Long memberId) {
seatService.selectSeat(seatId, memberId);
return ResponseEntity.ok().build();
return ResponseEntity.noContent().build();
}

@PostMapping("/{seatId}/deselect")
public ResponseEntity<Void> deselectSeat(@PathVariable Long seatId, @CurrentAccount Long memberId) {
seatService.deselectSeat(seatId, memberId);
return ResponseEntity.ok().build();
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.pgms.apibooking.domain.seat.service;

import java.time.Duration;
import java.util.List;
import java.util.Optional;

import org.springframework.data.redis.core.RedisTemplate;
Expand All @@ -10,35 +11,32 @@

@Service
@RequiredArgsConstructor
class SeatLockService { //TODO: 레디스 레포지토리 분리
public class SeatLockService {

private final static String SEAT_LOCK_CACHE_KEY_PREFIX = "seatId:";
private final static String SEAT_LOCK_CACHE_VALUE_PREFIX = "memberId:";
private final static int SEAT_LOCK_CACHE_EXPIRE_SECONDS = 420;

private final RedisTemplate<String, String> redisTemplate;

Optional<Long> getSelectorId(Long seatId) {
public Long getSelectorId(Long seatId) {
String key = generateSeatLockKey(seatId);
String value = redisTemplate.opsForValue().get(key);
if (value == null) {
return Optional.empty();
}
return Optional.of(extractMemberId(value));
return value == null ? null : extractMemberId(value);
}

boolean isSeatLocked(Long seatId) {
public boolean isSeatLocked(Long seatId) {
return redisTemplate.opsForValue().get(generateSeatLockKey(seatId)) != null;
}

void lockSeat(Long seatId, Long memberId) {
public void lockSeat(Long seatId, Long memberId) {
String key = generateSeatLockKey(seatId);
String value = generateSeatLockValue(memberId);
Duration timeout = Duration.ofSeconds(SEAT_LOCK_CACHE_EXPIRE_SECONDS);
redisTemplate.opsForValue().setIfAbsent(key, value, timeout);
}

void unlockSeat(Long seatId) {
public void unlockSeat(Long seatId) {
redisTemplate.delete(generateSeatLockKey(seatId));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.pgms.apibooking.domain.seat.service;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -36,13 +35,13 @@ public List<AreaResponse> getSeats(SeatsGetRequest request) {

public void selectSeat(Long seatId, Long memberId) {
if (seatLockService.isSeatLocked(seatId)) {
Optional<Long> selectorIdOpt = seatLockService.getSelectorId(seatId);
Long selectorId = seatLockService.getSelectorId(seatId);

if (selectorIdOpt.isPresent() && selectorIdOpt.get().equals(memberId)) {
if (selectorId != null && selectorId.equals(memberId)) {
return;
}

throw new BookingException(BookingErrorCode.SEAT_BEING_BOOKED);
throw new BookingException(BookingErrorCode.SEAT_SELECTED_BY_ANOTHER_MEMBER);
}

EventSeat seat = getSeat(seatId);
Expand All @@ -51,19 +50,19 @@ public void selectSeat(Long seatId, Long memberId) {
throw new BookingException(BookingErrorCode.SEAT_ALREADY_BOOKED);
}

seat.updateStatus(EventSeatStatus.BEING_BOOKED);
seat.updateStatus(EventSeatStatus.SELECTED);
seatLockService.lockSeat(seatId, memberId);
}

public void deselectSeat(Long seatId, Long memberId) {
Optional<Long> selectorIdOpt = seatLockService.getSelectorId(seatId);
Long selectorId = seatLockService.getSelectorId(seatId);

if (selectorIdOpt.isEmpty()) {
if (selectorId == null) {
updateSeatStatusToAvailable(seatId);
return;
}

if (!selectorIdOpt.get().equals(memberId)) {
if (!selectorId.equals(memberId)) {
throw new BookingException(BookingErrorCode.SEAT_SELECTED_BY_ANOTHER_MEMBER);
}

Expand Down
Loading