From e9835588af8db98dace434d71c03178e7f341d47 Mon Sep 17 00:00:00 2001 From: Hanna Lee <8annahxxl@gmail.com> Date: Sun, 14 Jan 2024 23:22:29 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=A2=8C=EC=84=9D=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=20=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EC=84=B8=EC=85=98=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EB=94=94=20=EA=B8=B0=EC=A4=80=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=84=A0=EC=A0=90=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#248)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 더미데이터 추가 * refactor: 큐 클랜징 로직 리팩터링 * fix: 좌석 선 택 시 토큰 세션 아이디 기준으로 선점하도록 * chore --- api/.DS_Store | Bin 0 -> 6148 bytes api/api-booking/http/booking.http | 14 ++++++++ .../booking/controller/BookingController.java | 2 +- .../booking/service/BookingService.java | 12 +++---- .../service/BookingQueueService.java | 31 +++++++++--------- .../seat/controller/SeatController.java | 16 ++++++--- .../domain/seat/service/SeatLockManager.java | 18 +++++----- .../domain/seat/service/SeatService.java | 14 ++++---- .../service/BookingServiceTest.java | 12 +++---- core/core-infra/src/main/resources/data.sql | 16 +++++---- db/initdb.d/2-data.sql | 6 +++- 11 files changed, 85 insertions(+), 56 deletions(-) create mode 100644 api/.DS_Store diff --git a/api/.DS_Store b/api/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..dc71a6d0f906244080c7496c140eeab0fc1d09ec GIT binary patch literal 6148 zcmeHK!AiqG5Z!I7Nhm@N3OxqAR*WT=;w8lT14i_qQWH{YXv~%-wTDv3S%1hc@q3)v z-H4_3;69j0f}Nj4_)rW`ZI%ENFfZ97o-dhV~$G`Hq+gcqS}m z`9?H3{v!jpcWoB2DeTz^`@VnUcu`E_OvDiJ9#6BhC|&o1H=3=zeZ#Pfw($@?iA7jO z<-7=@*&X(-g~;OCj^dkiv>4il7a}jCG#|}mLYj;q#`zGOeNA zSuQ=->pGt2uDZ^$-*dt4_f{**JUTu-zZy=m=UluhN;!OeQVulM@CL?~)xCtXJQMjC zBI@iqi;x%~28e-OXF#8OW_#BcO6wyAh=E@-faik)is);s6w0Fm8vK34@fIQq*m##f zbTs-JD}^uu!c{7uO6B^B!Bsl=9UbRutQ4wr#^ucL9z8SHHxw>s2fri38TS=ZOAHVL z>kKqi*T(vPx&8jXoWrK7yiPT&3_U1q@k=Ar?z< c2UH699W(%ajg><1fY6VCq=6b@;7=L&1pLEJ7ytkO literal 0 HcmV?d00001 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)