diff --git a/api/.DS_Store b/api/.DS_Store new file mode 100644 index 00000000..dc71a6d0 Binary files /dev/null and b/api/.DS_Store differ diff --git a/api/api-booking/http/booking.http b/api/api-booking/http/booking.http index a8dd6021..3fd23411 100644 --- a/api/api-booking/http/booking.http +++ b/api/api-booking/http/booking.http @@ -53,6 +53,20 @@ POST http://localhost:8082/api/v1/seats/1/deselect Authorization: Bearer {{accessToken}} Booking-Authorization: Bearer {{bookingToken}} +### 예매 생성 +POST http://localhost:8082/api/v1/bookings/create +Content-Type: application/json +Authorization: Bearer {{accessToken}} +Booking-Authorization: Bearer {{bookingToken}} + +{ + "timeId": 1, + "seatIds": [1], + "receiptType": "현장수령", + "buyerName": "빙봉", + "buyerPhoneNumber": "010-1234-5678" +} + ### 예매 ~ 결제 승인 (브라우저에서 진행해 주세요) GET http://localhost:8082/bookings 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 cf569cf8..046ef752 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 @@ -47,7 +47,7 @@ public ResponseEntity> createBooking( @RequestAttribute("tokenSessionId") String tokenSessionId, HttpServletRequest httpRequest) { BookingCreateResponse createdBooking = bookingService.createBooking(request, memberId, tokenSessionId); - ApiResponse response = ApiResponse.ok(createdBooking); + ApiResponse response = ApiResponse.created(createdBooking); URI location = UriComponentsBuilder .fromHttpUrl(httpRequest.getRequestURL().toString()) .path("/{id}") 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 3055dfcc..f93d0e80 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 @@ -68,7 +68,7 @@ public class BookingService { //TODO: 테스트 코드 작성 public BookingCreateResponse createBooking(BookingCreateRequest request, Long memberId, String tokenSessionId) { Member member = getMemberById(memberId); EventTime time = getBookableTimeWithEvent(request.timeId()); - List seats = getBookableSeatsWithArea(request.timeId(), request.seatIds(), memberId); + List seats = getBookableSeatsWithArea(request.timeId(), request.seatIds(), tokenSessionId); ReceiptType receiptType = ReceiptType.fromDescription(request.receiptType()); validateDeliveryAddress(receiptType, request.deliveryAddress()); @@ -188,8 +188,8 @@ private EventTime getBookableTimeWithEvent(Long timeId) { return time; } - private List getBookableSeatsWithArea(Long timeId, List seatIds, Long memberId) { - checkHeldSeats(seatIds, memberId); + private List getBookableSeatsWithArea(Long timeId, List seatIds, String tokenSessionId) { + checkHeldSeats(seatIds, tokenSessionId); List seats = eventSeatRepository.findAllWithAreaByTimeIdAndSeatIds(timeId, seatIds); @@ -204,10 +204,10 @@ private List getBookableSeatsWithArea(Long timeId, List seatIds return seats; } - private void checkHeldSeats(List seatIds, Long memberId) { + private void checkHeldSeats(List seatIds, String sessionId) { seatIds.forEach(seatId -> { - Long selectorId = seatLockManager.getSelectorId(seatId).orElse(null); - if (selectorId == null || !selectorId.equals(memberId)) { + String selectorId = seatLockManager.getSelectorId(seatId).orElse(null); + if (selectorId == null || !selectorId.equals(sessionId)) { throw new BookingException(BookingErrorCode.UNBOOKABLE_SEAT_INCLUSION); } }); diff --git a/api/api-booking/src/main/java/com/pgms/apibooking/domain/bookingqueue/service/BookingQueueService.java b/api/api-booking/src/main/java/com/pgms/apibooking/domain/bookingqueue/service/BookingQueueService.java index 302f2614..b89fd2a2 100644 --- a/api/api-booking/src/main/java/com/pgms/apibooking/domain/bookingqueue/service/BookingQueueService.java +++ b/api/api-booking/src/main/java/com/pgms/apibooking/domain/bookingqueue/service/BookingQueueService.java @@ -23,7 +23,7 @@ public class BookingQueueService { private final static double MILLISECONDS_PER_SECOND = 1000.0; private final static double TIMEOUT_SECONDS = 7 * 60; - private final static long ENTRY_LIMIT = 5; + private final static long ENTRY_LIMIT = 2; private final BookingQueueManager bookingQueueManager; private final BookingJwtProvider bookingJwtProvider; @@ -34,19 +34,17 @@ public void enterQueue(BookingQueueEnterRequest request, String sessionId) { } public OrderInQueueGetResponse getOrderInQueue(Long eventId, String sessionId) { + cleanQueue(eventId); Long order = getOrder(eventId, sessionId); - Long myOrder = order <= ENTRY_LIMIT ? 0 : order - ENTRY_LIMIT; - Boolean isMyTurn = isReadyToEnter(eventId, sessionId); - - double currentTimeSeconds = System.currentTimeMillis() / MILLISECONDS_PER_SECOND; - double timeLimitSeconds = currentTimeSeconds - TIMEOUT_SECONDS; - bookingQueueManager.removeRangeByScore(eventId, 0, timeLimitSeconds); - + Boolean isMyTurn = order <= ENTRY_LIMIT; + Long myOrder = isMyTurn ? 0 : order - ENTRY_LIMIT; return OrderInQueueGetResponse.of(myOrder, isMyTurn); } public TokenIssueResponse issueToken(TokenIssueRequest request, String sessionId) { - if (!isReadyToEnter(request.eventId(), sessionId)) { + Long order = getOrder(request.eventId(), sessionId); + + if (order > ENTRY_LIMIT) { throw new BookingException(BookingErrorCode.OUT_OF_ORDER); } @@ -61,12 +59,6 @@ private Long getOrder(Long eventId, String sessionId) { .orElseThrow(() -> new BookingException(BookingErrorCode.NOT_IN_QUEUE)); } - private Boolean isReadyToEnter(Long eventId, String sessionId) { - Long myOrder = getOrder(eventId, sessionId); - Long entryLimit = ENTRY_LIMIT; - return myOrder <= entryLimit; - } - public void exitQueue(BookingQueueExitRequest request, String sessionId) { bookingQueueManager.remove(request.eventId(), sessionId); } @@ -75,4 +67,13 @@ public SessionIdIssueResponse issueSessionId() { UUID sessionId = UUID.randomUUID(); return SessionIdIssueResponse.from(sessionId.toString()); } + + /* + * 대기열에 존재하는 세션 중 타임아웃된 세션을 제거한다. + */ + private void cleanQueue(Long eventId) { + double currentTimeSeconds = System.currentTimeMillis() / MILLISECONDS_PER_SECOND; + double timeLimitSeconds = currentTimeSeconds - TIMEOUT_SECONDS; + bookingQueueManager.removeRangeByScore(eventId, 0, timeLimitSeconds); + } } 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 b8e0680b..9950e945 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 @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -14,7 +15,6 @@ 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 io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -38,15 +38,21 @@ public ResponseEntity>> getSeats(@ModelAttribute @Operation(summary = "좌석 선택") @PostMapping("/{seatId}/select") - public ResponseEntity selectSeat(@PathVariable Long seatId, @CurrentAccount Long memberId) { - seatService.selectSeat(seatId, memberId); + public ResponseEntity selectSeat( + @PathVariable Long seatId, + @RequestAttribute("tokenSessionId") String tokenSessionId + ) { + seatService.selectSeat(seatId, tokenSessionId); return ResponseEntity.noContent().build(); } @Operation(summary = "좌석 선택 해제") @PostMapping("/{seatId}/deselect") - public ResponseEntity deselectSeat(@PathVariable Long seatId, @CurrentAccount Long memberId) { - seatService.deselectSeat(seatId, memberId); + public ResponseEntity deselectSeat( + @PathVariable Long seatId, + @RequestAttribute("tokenSessionId") String tokenSessionId + ) { + seatService.deselectSeat(seatId, tokenSessionId); return ResponseEntity.noContent().build(); } } diff --git a/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/service/SeatLockManager.java b/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/service/SeatLockManager.java index 3c38b0b8..b08d2dd9 100644 --- a/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/service/SeatLockManager.java +++ b/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/service/SeatLockManager.java @@ -13,19 +13,19 @@ public class SeatLockManager { private final static String SEAT_LOCK_CACHE_KEY_PREFIX = "seatId:"; - private final static String SEAT_LOCK_CACHE_VALUE_PREFIX = "memberId:"; + private final static String SEAT_LOCK_CACHE_VALUE_PREFIX = "sessionId:"; private final RedisOperator redisOperator; - public Optional getSelectorId(Long seatId) { + public Optional getSelectorId(Long seatId) { String key = generateSeatLockKey(seatId); String value = redisOperator.get(key); - return Optional.ofNullable(value == null ? null : extractMemberId(value)); + return Optional.ofNullable(value == null ? null : extractSessionId(value)); } - public void lockSeat(Long seatId, Long memberId, Integer expirationSeconds) { + public void lockSeat(Long seatId, String tokenSessionId, Integer expirationSeconds) { String key = generateSeatLockKey(seatId); - String value = generateSeatLockValue(memberId); + String value = generateSeatLockValue(tokenSessionId); redisOperator.setIfAbsent(key, value, expirationSeconds); } @@ -37,11 +37,11 @@ private String generateSeatLockKey(Long seatId) { return SEAT_LOCK_CACHE_KEY_PREFIX + seatId; } - private String generateSeatLockValue(Long memberId) { - return SEAT_LOCK_CACHE_VALUE_PREFIX + memberId; + private String generateSeatLockValue(String tokenSessionId) { + return SEAT_LOCK_CACHE_VALUE_PREFIX + tokenSessionId; } - private Long extractMemberId(String value) { - return Long.parseLong(value.replace(SEAT_LOCK_CACHE_VALUE_PREFIX, "")); + private String extractSessionId(String value) { + return 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 61871089..50e10215 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 @@ -35,11 +35,11 @@ public List getSeats(SeatsGetRequest request) { .toList(); } - public void selectSeat(Long seatId, Long memberId) { - Long selectorId = seatLockManager.getSelectorId(seatId).orElse(null); + public void selectSeat(Long seatId, String tokenSessionId) { + String selectorId = seatLockManager.getSelectorId(seatId).orElse(null); if (selectorId != null) { - if (selectorId.equals(memberId)) { + if (selectorId.equals(tokenSessionId)) { return; } throw new BookingException(BookingErrorCode.SEAT_HELD_BY_ANOTHER_MEMBER); @@ -52,18 +52,18 @@ public void selectSeat(Long seatId, Long memberId) { } seat.updateStatus(EventSeatStatus.HOLDING); - seatLockManager.lockSeat(seatId, memberId, SEAT_LOCK_CACHE_EXPIRE_SECONDS); + seatLockManager.lockSeat(seatId, tokenSessionId, SEAT_LOCK_CACHE_EXPIRE_SECONDS); } - public void deselectSeat(Long seatId, Long memberId) { - Long selectorId = seatLockManager.getSelectorId(seatId).orElse(null); + public void deselectSeat(Long seatId, String tokenSessionId) { + String selectorId = seatLockManager.getSelectorId(seatId).orElse(null); if (selectorId == null) { updateSeatStatusToAvailable(seatId); return; } - if (!selectorId.equals(memberId)) { + if (!selectorId.equals(tokenSessionId)) { throw new BookingException(BookingErrorCode.SEAT_HELD_BY_ANOTHER_MEMBER); } diff --git a/api/api-booking/src/test/java/com/pgms/apibooking/service/BookingServiceTest.java b/api/api-booking/src/test/java/com/pgms/apibooking/service/BookingServiceTest.java index 379cc64f..28799afb 100644 --- a/api/api-booking/src/test/java/com/pgms/apibooking/service/BookingServiceTest.java +++ b/api/api-booking/src/test/java/com/pgms/apibooking/service/BookingServiceTest.java @@ -161,7 +161,7 @@ void setup() { Optional.empty() ); - given(seatLockManager.getSelectorId(any(Long.class))).willReturn(Optional.of(member.getId())); + given(seatLockManager.getSelectorId(any(Long.class))).willReturn(Optional.of(SESSION_ID)); doNothing().when(bookingQueueManager).remove(any(Long.class), any(String.class)); // when @@ -243,7 +243,7 @@ void setup() { ); given(seatLockManager.getSelectorId(seat1.getId())).willReturn(Optional.empty()); - given(seatLockManager.getSelectorId(seat2.getId())).willReturn(Optional.of(member.getId() + 1)); + given(seatLockManager.getSelectorId(seat2.getId())).willReturn(Optional.of(SESSION_ID)); // when & then assertThatThrownBy(() -> bookingService.createBooking(request, member.getId(), SESSION_ID)) @@ -295,7 +295,7 @@ void setup() { Optional.empty() ); - given(seatLockManager.getSelectorId(seat.getId())).willReturn(Optional.of(member.getId())); + given(seatLockManager.getSelectorId(seat.getId())).willReturn(Optional.of(SESSION_ID)); // when & then assertThatThrownBy(() -> bookingService.createBooking(request, member.getId(), SESSION_ID)) @@ -349,7 +349,7 @@ void setup() { Optional.empty() ); - given(seatLockManager.getSelectorId(seat.getId())).willReturn(Optional.of(member.getId())); + given(seatLockManager.getSelectorId(seat.getId())).willReturn(Optional.of(SESSION_ID)); // when & then assertThatThrownBy(() -> bookingService.createBooking(request, member.getId(), SESSION_ID)) @@ -401,7 +401,7 @@ void setup() { Optional.empty() ); - given(seatLockManager.getSelectorId(seat.getId())).willReturn(Optional.of(member.getId())); + given(seatLockManager.getSelectorId(seat.getId())).willReturn(Optional.of(SESSION_ID)); // when & then assertThatThrownBy(() -> bookingService.createBooking(request, member.getId(), SESSION_ID)) @@ -453,7 +453,7 @@ void setup() { Optional.empty() ); - given(seatLockManager.getSelectorId(seat.getId())).willReturn(Optional.of(member.getId())); + given(seatLockManager.getSelectorId(seat.getId())).willReturn(Optional.of(SESSION_ID)); // when & then assertThatThrownBy(() -> bookingService.createBooking(request, member.getId(), SESSION_ID)) diff --git a/core/core-infra/src/main/resources/data.sql b/core/core-infra/src/main/resources/data.sql index 3e53dc8a..a4eaabd3 100644 --- a/core/core-infra/src/main/resources/data.sql +++ b/core/core-infra/src/main/resources/data.sql @@ -21,12 +21,16 @@ VALUES (100000, 'S', 1), -- EventSeat INSERT INTO event_seat (name, status, event_seat_area_id, event_time_id) -VALUES ('A1', 'AVAILABLE', 1, 1) - , ('A2', 'AVAILABLE', 1, 1) - , ('A3', 'AVAILABLE', 1, 1) - , ('E1', 'AVAILABLE', 2, 1) - , ('E2', 'AVAILABLE', 2, 1) - , ('E3', 'AVAILABLE', 2, 1); +VALUES ('A1', 'AVAILABLE', 1, 1), + ('A2', 'AVAILABLE', 1, 1), + ('A3', 'AVAILABLE', 1, 1), + ('A4', 'AVAILABLE', 1, 1), + ('A5', 'AVAILABLE', 1, 1), + ('E1', 'AVAILABLE', 2, 1), + ('E2', 'AVAILABLE', 2, 1), + ('E3', 'AVAILABLE', 2, 1), + ('E4', 'BOOKED', 2, 1), + ('E5', 'BOOKED', 2, 1); -- EventReview INSERT INTO event_review (score, content, event_id) diff --git a/db/initdb.d/2-data.sql b/db/initdb.d/2-data.sql index 02156cbc..ed484a1a 100644 --- a/db/initdb.d/2-data.sql +++ b/db/initdb.d/2-data.sql @@ -24,9 +24,13 @@ INSERT INTO event_seat (name, status, event_seat_area_id, event_time_id) VALUES ('A1', 'AVAILABLE', 1, 1), ('A2', 'AVAILABLE', 1, 1), ('A3', 'AVAILABLE', 1, 1), + ('A4', 'AVAILABLE', 1, 1), + ('A5', 'AVAILABLE', 1, 1), ('E1', 'AVAILABLE', 2, 1), ('E2', 'AVAILABLE', 2, 1), - ('E3', 'AVAILABLE', 2, 1); + ('E3', 'AVAILABLE', 2, 1), + ('E4', 'BOOKED', 2, 1), + ('E5', 'BOOKED', 2, 1); -- EventReview INSERT INTO event_review (score, content, event_id)