diff --git a/src/main/java/com/profect/tickle/domain/event/controller/EventController.java b/src/main/java/com/profect/tickle/domain/event/controller/EventController.java index 885867c5..f84a8a09 100644 --- a/src/main/java/com/profect/tickle/domain/event/controller/EventController.java +++ b/src/main/java/com/profect/tickle/domain/event/controller/EventController.java @@ -68,9 +68,9 @@ public ResultResponse createTicketEvent(@Valid @RequestB content = @Content(schema = @Schema(implementation = TicketApplyResponseDto.class))), @ApiResponse(responseCode = "400", description = "포인트 부족, 중복 응모 등 예외 발생")}) @PostMapping("/ticket/{eventId}") - public ResultResponse applyTicketEvent(@PathVariable Long eventId) { - eventService.applyTicketEvent(eventId); - return ResultResponse.of(ResultCode.EVENT_APPLY_SUCCESS, "당첨 됐게 안됐게"); + public ResultResponse applyTicketEvent(@PathVariable Long eventId) { + TicketApplyResponseDto dto = eventService.applyTicketEvent(eventId); + return ResultResponse.of(ResultCode.EVENT_APPLY_SUCCESS, dto); } @Operation(summary = "쿠폰 이벤트 응모", description = "유저가 쿠폰 이벤트에 응모하여 쿠폰을 발급받습니다.", diff --git a/src/main/java/com/profect/tickle/domain/event/dto/response/TicketApplyResponseDto.java b/src/main/java/com/profect/tickle/domain/event/dto/response/TicketApplyResponseDto.java index 91598d04..dab5d770 100644 --- a/src/main/java/com/profect/tickle/domain/event/dto/response/TicketApplyResponseDto.java +++ b/src/main/java/com/profect/tickle/domain/event/dto/response/TicketApplyResponseDto.java @@ -3,15 +3,13 @@ public record TicketApplyResponseDto( Long eventId, Long memberId, - boolean isWinner, String message ) { - public static TicketApplyResponseDto from(Long eventId, Long memberId, boolean isWinner) { + public static TicketApplyResponseDto from(Long eventId, Long memberId) { return new TicketApplyResponseDto( eventId, memberId, - isWinner, - isWinner ? "축하합니다! 티켓에 당첨되었습니다. \n 예매권은 마이페이지에서 확인하세요." : "아쉽네요. 다음 기회에..." + "성공적으로 응모되었습니다. 마이페이지에서 결과를 확인하세요." ); } } \ No newline at end of file diff --git a/src/main/java/com/profect/tickle/domain/event/service/event/EventCoreLockService.java b/src/main/java/com/profect/tickle/domain/event/service/event/EventCoreLockService.java index 67d0ca29..486b3e42 100644 --- a/src/main/java/com/profect/tickle/domain/event/service/event/EventCoreLockService.java +++ b/src/main/java/com/profect/tickle/domain/event/service/event/EventCoreLockService.java @@ -27,6 +27,7 @@ public class EventCoreLockService { //TODO: 임계영역에 대해서 동시성을 보장하면 원하는 결과가 나올겁니다? //TODO: 영한님의 고급 1편을 보세요. 자바 코드에 대한 동시성을 찾아보세요 + @Transactional(propagation = Propagation.REQUIRES_NEW) public EventDecision applyCore(Long eventId, Long memberId) { Event event = eventRepository.findById(eventId) @@ -47,17 +48,21 @@ public EventDecision applyCore(Long eventId, Long memberId) { eventId, delta, StatusIds.Event.IN_PROGRESS, StatusIds.Event.COMPLETED); - if (rows.isEmpty()) - throw new BusinessException(ErrorCode.EVENT_ALREADY_COMPLETED); + + if (rows.isEmpty()) throw new BusinessException(ErrorCode.EVENT_ALREADY_COMPLETED); var r = rows.get(0); boolean completed = r.getStatusId().equals(StatusIds.Event.COMPLETED); + Long seatId = null; + boolean winner = false; - // 4) 좌석 발급 (종료시에만 시도) if (completed) { seatId = reservationService.assignSeatForWinner(eventId, memberId); + System.out.println("seatId = " + seatId); + winner = (seatId != null); // ★ 좌석 배정 성공 시에만 winner } - return new EventDecision(eventId, memberId, delta, completed, r.getEventAccrued(), seatId); + + return new EventDecision(eventId, memberId, delta, winner, r.getEventAccrued(), seatId); } } \ No newline at end of file diff --git a/src/main/java/com/profect/tickle/domain/event/service/event/EventService.java b/src/main/java/com/profect/tickle/domain/event/service/event/EventService.java index 046661fe..2fe9e4e3 100644 --- a/src/main/java/com/profect/tickle/domain/event/service/event/EventService.java +++ b/src/main/java/com/profect/tickle/domain/event/service/event/EventService.java @@ -14,7 +14,7 @@ public interface EventService { TicketEventResponseDto createTicketEvent(TicketEventCreateRequestDto request); - void applyTicketEvent(Long eventId); + TicketApplyResponseDto applyTicketEvent(Long eventId); PagingResponse getEventList(EventType type, int page, int size); diff --git a/src/main/java/com/profect/tickle/domain/event/service/event/PostActionsService.java b/src/main/java/com/profect/tickle/domain/event/service/event/PostActionsService.java index acb5f90c..962058b2 100644 --- a/src/main/java/com/profect/tickle/domain/event/service/event/PostActionsService.java +++ b/src/main/java/com/profect/tickle/domain/event/service/event/PostActionsService.java @@ -7,8 +7,10 @@ import com.profect.tickle.domain.point.entity.PointTarget; import com.profect.tickle.domain.point.repository.PointRepository; import com.profect.tickle.domain.reservation.entity.Reservation; +import com.profect.tickle.domain.reservation.entity.Seat; import com.profect.tickle.domain.reservation.repository.ReservationRepository; import com.profect.tickle.domain.reservation.repository.SeatRepository; +import com.profect.tickle.global.status.Status; import com.profect.tickle.global.status.StatusIds; import com.profect.tickle.global.status.service.StatusProvider; import lombok.RequiredArgsConstructor; @@ -38,12 +40,6 @@ public void recordPointHistory(Long memberId, int amount, PointTarget target) { @Transactional(propagation = Propagation.REQUIRES_NEW) public void reserveSeatAndCreateReservation(Long seatId, Long memberId, int accrued) { - final Long RESERVED = statusProvider.provide(StatusIds.Seat.RESERVED).getId(); - final Long AVAILABLE = statusProvider.provide(StatusIds.Seat.AVAILABLE).getId(); - - int updated = seatRepository.tryReserveSeat(seatId, memberId, RESERVED, AVAILABLE); - if (updated == 0) return; - Long perfId = seatRepository.findPerformanceIdBySeatId(seatId); Reservation r = Reservation.create( memberRepository.getReferenceById(memberId), diff --git a/src/main/java/com/profect/tickle/domain/event/service/event/impl/EventServiceImpl.java b/src/main/java/com/profect/tickle/domain/event/service/event/impl/EventServiceImpl.java index 2fcd1cdb..51f63b6a 100644 --- a/src/main/java/com/profect/tickle/domain/event/service/event/impl/EventServiceImpl.java +++ b/src/main/java/com/profect/tickle/domain/event/service/event/impl/EventServiceImpl.java @@ -104,10 +104,12 @@ public TicketEventResponseDto createTicketEvent(TicketEventCreateRequestDto requ } @Override - public void applyTicketEvent(Long eventId) { + public TicketApplyResponseDto applyTicketEvent(Long eventId) { Long memberId = SecurityUtil.getSignInMemberId(); producer.appendToStream(new EventMessage(eventId, memberId)); + + return TicketApplyResponseDto.from(eventId, memberId); } @Override diff --git a/src/main/java/com/profect/tickle/domain/reservation/repository/SeatRepository.java b/src/main/java/com/profect/tickle/domain/reservation/repository/SeatRepository.java index eec632a7..b739e831 100644 --- a/src/main/java/com/profect/tickle/domain/reservation/repository/SeatRepository.java +++ b/src/main/java/com/profect/tickle/domain/reservation/repository/SeatRepository.java @@ -1,5 +1,6 @@ package com.profect.tickle.domain.reservation.repository; +import com.profect.tickle.domain.member.entity.Member; import com.profect.tickle.domain.performance.entity.HallType; import com.profect.tickle.domain.reservation.dto.response.reservation.SeatInfoResponseDto; import com.profect.tickle.domain.reservation.entity.Seat; @@ -96,40 +97,58 @@ long countReservedSeatsByUserAndPerformance(@Param("memberId") Long memberId, @Modifying(clearAutomatically = true, flushAutomatically = true) @Query(""" - update Seat s - set s.member.id = :memberId, - s.status.id = :reservedStatusId - where s.id = :seatId - and s.status.id = :availableStatusId - and (s.member.id is null or s.member.id = :memberId) - """) - int tryReserveSeat(@Param("seatId") Long seatId, - @Param("memberId") Long memberId, - @Param("reservedStatusId") Long reservedStatusId, - @Param("availableStatusId") Long availableStatusId); + update Seat s + set s.member = :member, + s.status = :reservedStatus + where s.id = :seatId + and s.status = :reservedStatus + and (s.member is null or s.member = :member) +""") + int tryReserveSeatWhenReserved(@Param("seatId") Long seatId, + @Param("member") Member member, + @Param("reservedStatus") Status reservedStatus); @Query(""" select s.performance.id from Seat s where s.id = :seatId """) + Long findPerformanceIdBySeatId(@Param("seatId") Long seatId); @Modifying(clearAutomatically = true, flushAutomatically = true) @Query(value = """ - UPDATE seat - SET member_id = :memberId, - status_id = :reserved, - seat_code = :seatCode - WHERE event_id = :eventId - AND member_id IS NULL - AND status_id = :available - RETURNING seat_id AS seatId - """, nativeQuery = true) - List assignSeatOnce(@Param("eventId") Long eventId, - @Param("memberId") Long memberId, - @Param("reserved") Long reserved, - @Param("available") Long available, - @Param("seatCode") String seatCode); + UPDATE seat + SET member_id = :memberId, + status_id = :reserved, + seat_code = :seatCode + WHERE seat_id = ( + SELECT seat_id + FROM seat + WHERE event_id = :eventId + AND member_id IS NULL + AND status_id = :available + ORDER BY seat_id + LIMIT 1 + ) + RETURNING seat_id AS seatId + """, nativeQuery = true) + List assignSeatEventOnce(@Param("eventId") Long eventId, + @Param("memberId") Long memberId, + @Param("reserved") Long reserved, + @Param("available") Long available, + @Param("seatCode") String seatCode); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(""" + update Seat s + set s.member.id = :memberId + where s.id = :seatId +""") + int assignPreReservedSeatToMember(@Param("seatId") Long seatId, + @Param("memberId") Long memberId); + + @Query(value = "select seat_id from seat where event_id = :eventId limit 1", nativeQuery = true) + Long findSeatIdByEvent(@Param("eventId") Long eventId); } \ No newline at end of file diff --git a/src/main/java/com/profect/tickle/domain/reservation/service/ReservationService.java b/src/main/java/com/profect/tickle/domain/reservation/service/ReservationService.java index 6e4f55a1..8d501348 100644 --- a/src/main/java/com/profect/tickle/domain/reservation/service/ReservationService.java +++ b/src/main/java/com/profect/tickle/domain/reservation/service/ReservationService.java @@ -118,23 +118,20 @@ public ReservationCompletionResponseDto completeReservation( @Transactional public Long assignSeatForWinner(Long eventId, Long memberId) { - String seatCode = generateSeatCode(); // 기존 메서드 활용 - - var rows = seatRepository.assignSeatOnce( - eventId, - memberId, - StatusIds.Seat.RESERVED, - StatusIds.Seat.AVAILABLE, - seatCode - ); - - if (rows.isEmpty()) { - log.info("Event {}: member {} 좌석 발급 실패 (경쟁에서 탈락)", eventId, memberId); - return null; // 다른 트랜잭션이 이미 선점함 + Long seatId = /* event.getSeat().getId() 혹은 */ seatRepository.findSeatIdByEvent(eventId); + if (seatId == null) { + log.warn("Event {}: 좌석 미지정", eventId); + return null; } - Long seatId = rows.get(0).getSeatId(); - log.info("Event {}: member {} 좌석 {} 발급 성공", eventId, memberId, seatId); + // 2) 당첨자에게 좌석 ‘배정’ (상태는 이미 RESERVED(13)) + final Long RESERVED = statusProvider.provide(StatusIds.Seat.RESERVED).getId(); + int updated = seatRepository.assignPreReservedSeatToMember(seatId, memberId); + if (updated == 0) { + log.error("Event {}: seat {} 배정 실패 (경합 탈락 or 상태 변경)", eventId, seatId); + return null; + } + log.error("Event {}: member {} 좌석 {} 배정 성공", eventId, memberId, seatId); return seatId; } diff --git a/src/main/resources/mapper/event/EventMapper.xml b/src/main/resources/mapper/event/EventMapper.xml index 006496c8..26f53f3f 100644 --- a/src/main/resources/mapper/event/EventMapper.xml +++ b/src/main/resources/mapper/event/EventMapper.xml @@ -42,20 +42,20 @@ SELECT COUNT(*) FROM event e - JOIN status st ON e.status_id = s.status_id - JOIN seat s ON s.event_id = e.event_id - JOIN performance p ON s.performance_id = p.performance_id + JOIN status st ON e.status_id = st.status_id + JOIN seat s ON s.event_id = e.event_id + JOIN performance p ON s.performance_id = p.performance_id WHERE e.event_type = 1 AND p.performance_start_date <= NOW() AND p.performance_end_date >= NOW()