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

Feature: 화면 구현에 필요한 API를 추가 구현한다. #128

Merged
merged 11 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
}
}
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);

default void releaseSeat(String memberEmail, SeatSelectionRequest seatSelectionRequest) {}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

디폴트 메서드로 정의하신 이유가 있나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정했습니다~

}
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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

밖에서 주입하고 final로 선언하는 건 어떤가요?


@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
Loading
Loading