Skip to content

Commit

Permalink
Feature: 화면 구현에 필요한 API를 추가 구현한다. (#128)
Browse files Browse the repository at this point in the history
  • Loading branch information
lass9436 authored Aug 22, 2024
2 parents d47a5b6 + 657a031 commit 8c78d88
Show file tree
Hide file tree
Showing 26 changed files with 358 additions and 32 deletions.
2 changes: 1 addition & 1 deletion backend-config
16 changes: 16 additions & 0 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ operation::user-performance-controller-test/get-performances[snippets='http-requ

operation::user-performance-controller-test/get-performances[snippets='http-response,response-fields']

== 공연 상세 조회

=== request

operation::user-performance-controller-test/get-performance[snippets='http-request']

=== response

operation::user-performance-controller-test/get-performance[snippets='http-response,response-fields']

= 좌석(Seat)

== 관리자 좌석 등급 생성 API
Expand Down Expand Up @@ -124,6 +134,12 @@ operation::ticket-controller-test/select-my-tickets[snippets='http-request,reque
=== response
operation::ticket-controller-test/select-my-tickets[snippets='http-response,response-fields']

== 좌석 점유 해제
operation::ticket-controller-test/release-seat[snippets='http-request,request-headers']

=== response
operation::ticket-controller-test/release-seat[snippets='http-response']

= 대기열(WaitingSystem)

== 남은 순번 조회
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ public enum ErrorCode {
Seat Error
*/
NOT_FOUND_SEAT(HttpStatus.NOT_FOUND, "S404-1", "존재하지 않는 좌석입니다."),
NOT_SELECTABLE_SEAT(HttpStatus.FORBIDDEN, "S403-1", "이미 선택된 좌석입니다."),
INVALID_SEAT_STATUS(HttpStatus.FORBIDDEN, "S403-2", "해당 좌석에는 접근할 수 없습니다."),
NOT_SELECTABLE_SEAT(HttpStatus.CONFLICT, "S409-1", "이미 선택된 좌석입니다."),
INVALID_SEAT_STATUS(HttpStatus.CONFLICT, "S409-2", "해당 좌석에는 접근할 수 없습니다."),

/*
Payment Error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

Expand All @@ -22,4 +23,10 @@ public class UserPerformanceController {
public ResponseEntity<ItemResult<PerformanceElement>> getPerformances() {
return ResponseEntity.ok(userPerformanceService.getPerformances());
}

@GetMapping("/{performanceId}")
public ResponseEntity<PerformanceElement> getPerformance(
@PathVariable("performanceId") long performanceId) {
return ResponseEntity.ok(userPerformanceService.getPerformance(performanceId));
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.thirdparty.ticketing.domain.performance.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.thirdparty.ticketing.domain.ItemResult;
import com.thirdparty.ticketing.domain.common.ErrorCode;
import com.thirdparty.ticketing.domain.common.TicketingException;
import com.thirdparty.ticketing.domain.performance.dto.PerformanceElement;
import com.thirdparty.ticketing.domain.performance.repository.PerformanceRepository;

Expand All @@ -14,8 +17,18 @@ public class UserPerformanceService {

private final PerformanceRepository performanceRepository;

@Transactional(readOnly = true)
public ItemResult<PerformanceElement> getPerformances() {
return ItemResult.of(
performanceRepository.findAll().stream().map(PerformanceElement::of).toList());
}

@Transactional(readOnly = true)
public PerformanceElement getPerformance(long performanceId) {

return performanceRepository
.findById(performanceId)
.map(PerformanceElement::of)
.orElseThrow(() -> new TicketingException(ErrorCode.NOT_FOUND_PERFORMANCE));
}
}
9 changes: 8 additions & 1 deletion src/main/java/com/thirdparty/ticketing/domain/seat/Seat.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ public void markAsPaid() {
}

public boolean isAssignedByMember(Member loginMember) {
return loginMember.equals(member);
return loginMember.getMemberId().equals(member.getMemberId());
}

public void releaseSeat(Member loginMember) {
if (!seatStatus.isSelected() || !isAssignedByMember(loginMember)) {
return;
}
this.seatStatus = SeatStatus.SELECTABLE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@
public interface SeatRepository extends JpaRepository<Seat, Long> {
List<Seat> findByZone(Zone zone);

@Query("SELECT s FROM Seat as s WHERE s.id = :seatId")
@Query("SELECT s FROM Seat as s WHERE s.seatId = :seatId")
@Lock(LockModeType.NONE)
Optional<Seat> findById(@Param("seatId") Long seatId);

@Query("SELECT s FROM Seat as s WHERE s.id = :seatId")
@Query("SELECT s FROM Seat as s WHERE s.seatId = :seatId")
@Lock(LockModeType.OPTIMISTIC)
Optional<Seat> findByIdWithOptimistic(@Param("seatId") Long seatId);

@Query("SELECT s FROM Seat as s WHERE s.id = :seatId")
@Query("SELECT s FROM Seat as s WHERE s.seatId = :seatId")
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Seat> findByIdWithPessimistic(@Param("seatId") Long seatId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@
import com.thirdparty.ticketing.domain.member.Member;
import com.thirdparty.ticketing.domain.seat.Seat;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.*;

@Getter
@Entity
@Builder
@AllArgsConstructor
@Table(name = "ticket")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ public ResponseEntity<ItemResult<TicketElement>> selectMyTickets(
return ResponseEntity.ok().body(tickets);
}

@PostMapping("/seats/release")
public ResponseEntity<Void> releaseSeat(
@LoginMember String memberEmail,
@RequestBody @Valid SeatSelectionRequest seatSelectionRequest) {
reservationService.releaseSeat(memberEmail, seatSelectionRequest);
return ResponseEntity.ok().build();
}

@PostMapping("/seats/select")
public ResponseEntity<Void> selectSeat(
@LoginMember String memberEmail,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,26 @@

import java.util.UUID;

import com.thirdparty.ticketing.domain.performance.Performance;
import com.thirdparty.ticketing.domain.performance.dto.PerformanceElement;
import com.thirdparty.ticketing.domain.seat.Seat;
import com.thirdparty.ticketing.domain.ticket.Ticket;

import lombok.Data;

@Data
public class TicketElement {
private final UUID serialNumber;
private final PerformanceElement performance;
private final TicketSeatDetail seat;

public static TicketElement of(Ticket ticket) {
return new TicketElement(ticket.getTicketSerialNumber());
Seat seat = ticket.getSeat();
Performance performance = seat.getZone().getPerformance();

return new TicketElement(
ticket.getTicketSerialNumber(),
PerformanceElement.of(performance),
TicketSeatDetail.of(seat));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.thirdparty.ticketing.domain.ticket.dto.response;

import com.thirdparty.ticketing.domain.seat.Seat;
import com.thirdparty.ticketing.domain.seat.dto.response.SeatGradeElement;

import lombok.Data;

@Data
public class TicketSeatDetail {
private final long seatId;
private final String seatCode;
private final SeatGradeElement grade;

public static TicketSeatDetail of(Seat seat) {
return new TicketSeatDetail(
seat.getSeatId(), seat.getSeatCode(), SeatGradeElement.of(seat.getSeatGrade()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,23 @@
import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import com.thirdparty.ticketing.domain.member.Member;
import com.thirdparty.ticketing.domain.ticket.Ticket;

import io.lettuce.core.dynamic.annotation.Param;

public interface TicketRepository extends JpaRepository<Ticket, Integer> {
List<Ticket> findAllByMember(Member member);
@Query(
"""
SELECT t FROM Ticket t
JOIN FETCH t.member m
JOIN FETCH t.seat s
JOIN FETCH s.seatGrade sg
JOIN FETCH s.zone z
JOIN FETCH z.performance p
WHERE t.member = :member
""")
List<Ticket> findAllByMember(@Param("member") Member member);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.thirdparty.ticketing.domain.ticket.service;

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.thirdparty.ticketing.domain.common.ErrorCode;
import com.thirdparty.ticketing.domain.common.TicketingException;
import com.thirdparty.ticketing.domain.member.Member;
import com.thirdparty.ticketing.domain.seat.Seat;
import com.thirdparty.ticketing.domain.seat.repository.SeatRepository;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class ReservationManager {
private final SeatRepository seatRepository;

@Transactional
public void releaseSeat(Member loginMember, long seatId) {
Seat seat =
seatRepository
.findById(seatId)
.orElseThrow(() -> new TicketingException(ErrorCode.NOT_FOUND_SEAT));
seat.releaseSeat(loginMember);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ public interface ReservationService {
void selectSeat(String memberEmail, SeatSelectionRequest seatSelectionRequest);

void reservationTicket(String memberEmail, TicketPaymentRequest ticketPaymentRequest);

void releaseSeat(String memberEmail, SeatSelectionRequest seatSelectionRequest);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package com.thirdparty.ticketing.domain.ticket.service;

import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.transaction.annotation.Transactional;

import com.thirdparty.ticketing.domain.common.ErrorCode;
Expand All @@ -10,9 +16,11 @@
import com.thirdparty.ticketing.domain.payment.PaymentProcessor;
import com.thirdparty.ticketing.domain.payment.dto.PaymentRequest;
import com.thirdparty.ticketing.domain.seat.Seat;
import com.thirdparty.ticketing.domain.ticket.Ticket;
import com.thirdparty.ticketing.domain.ticket.dto.event.PaymentEvent;
import com.thirdparty.ticketing.domain.ticket.dto.request.SeatSelectionRequest;
import com.thirdparty.ticketing.domain.ticket.dto.request.TicketPaymentRequest;
import com.thirdparty.ticketing.domain.ticket.repository.TicketRepository;
import com.thirdparty.ticketing.domain.ticket.service.strategy.LockSeatStrategy;

import lombok.RequiredArgsConstructor;
Expand All @@ -21,11 +29,18 @@
@Slf4j
@RequiredArgsConstructor
public class ReservationTransactionService implements ReservationService {
private final TicketRepository ticketRepository;
private final MemberRepository memberRepository;
private final PaymentProcessor paymentProcessor;
private final LockSeatStrategy lockSeatStrategy;
private final EventPublisher eventPublisher;

private final ReservationManager reservationManager;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5);

@Value("${ticketing.reservation.release-delay-seconds}")
private int reservationReleaseDelay;

@Override
@Transactional
public void selectSeat(String memberEmail, SeatSelectionRequest seatSelectionRequest) {
Expand All @@ -42,6 +57,21 @@ public void selectSeat(String memberEmail, SeatSelectionRequest seatSelectionReq
.orElseThrow(() -> new TicketingException(ErrorCode.NOT_FOUND_MEMBER));

seat.assignByMember(member);
scheduler.schedule(
() -> reservationManager.releaseSeat(member, seatId),
reservationReleaseDelay,
TimeUnit.SECONDS);
}

@Override
@Transactional
public void releaseSeat(String memberEmail, SeatSelectionRequest seatSelectionRequest) {
Member member =
memberRepository
.findByEmail(memberEmail)
.orElseThrow(() -> new TicketingException(ErrorCode.NOT_FOUND_MEMBER));

reservationManager.releaseSeat(member, seatSelectionRequest.getSeatId());
}

@Override
Expand All @@ -60,12 +90,21 @@ public void reservationTicket(String memberEmail, TicketPaymentRequest ticketPay

processPayment(seat, loginMember);

if (seat.isAssignedByMember(loginMember)) {
throw new TicketingException(ErrorCode.NOT_SELECTABLE_SEAT);
}
Ticket ticket =
Ticket.builder()
.ticketSerialNumber(UUID.randomUUID())
.seat(seat)
.member(loginMember)
.build();

ticketRepository.save(ticket);
}

private void processPayment(Seat seat, Member loginMember) {
if (!seat.isAssignedByMember(loginMember)) {
throw new TicketingException(ErrorCode.NOT_SELECTABLE_SEAT);
}

seat.markAsPendingPayment();
paymentProcessor.processPayment(new PaymentRequest());
seat.markAsPaid();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,9 @@ public void reservationTicket(String memberEmail, TicketPaymentRequest ticketPay
reservationTransactionService.reservationTicket(
memberEmail, ticketPaymentRequest));
}

@Override
public void releaseSeat(String memberEmail, SeatSelectionRequest seatSelectionRequest) {
reservationTransactionService.releaseSeat(memberEmail, seatSelectionRequest);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public void selectSeat(String memberEmail, SeatSelectionRequest seatSelectionReq
reservationTransactionService.selectSeat(memberEmail, seatSelectionRequest);
} catch (OptimisticLockingFailureException | StaleObjectStateException e) {
log.error(e.getMessage(), e);
throw new TicketingException(ErrorCode.INTERNAL_SERVER_ERROR);
throw new TicketingException(ErrorCode.NOT_SELECTABLE_SEAT);
}
}

Expand All @@ -33,7 +33,12 @@ public void reservationTicket(String memberEmail, TicketPaymentRequest ticketPay
reservationTransactionService.reservationTicket(memberEmail, ticketPaymentRequest);
} catch (OptimisticLockingFailureException | StaleObjectStateException e) {
log.error(e.getMessage(), e);
throw new TicketingException(ErrorCode.INTERNAL_SERVER_ERROR);
throw new TicketingException(ErrorCode.NOT_SELECTABLE_SEAT);
}
}

@Override
public void releaseSeat(String memberEmail, SeatSelectionRequest seatSelectionRequest) {
reservationTransactionService.releaseSeat(memberEmail, seatSelectionRequest);
}
}
Loading

0 comments on commit 8c78d88

Please sign in to comment.