Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ public ResultResponse<TicketEventResponseDto> createTicketEvent(@Valid @RequestB
content = @Content(schema = @Schema(implementation = TicketApplyResponseDto.class))),
@ApiResponse(responseCode = "400", description = "포인트 부족, 중복 응모 등 예외 발생")})
@PostMapping("/ticket/{eventId}")
public ResultResponse<String> applyTicketEvent(@PathVariable Long eventId) {
eventService.applyTicketEvent(eventId);
return ResultResponse.of(ResultCode.EVENT_APPLY_SUCCESS, "당첨 됐게 안됐게");
public ResultResponse<TicketApplyResponseDto> applyTicketEvent(@PathVariable Long eventId) {
TicketApplyResponseDto dto = eventService.applyTicketEvent(eventId);
return ResultResponse.of(ResultCode.EVENT_APPLY_SUCCESS, dto);
}

@Operation(summary = "쿠폰 이벤트 응모", description = "유저가 쿠폰 이벤트에 응모하여 쿠폰을 발급받습니다.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 예매권은 마이페이지에서 확인하세요." : "아쉽네요. 다음 기회에..."
"성공적으로 응모되었습니다. 마이페이지에서 결과를 확인하세요."
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public interface EventService {

TicketEventResponseDto createTicketEvent(TicketEventCreateRequestDto request);

void applyTicketEvent(Long eventId);
TicketApplyResponseDto applyTicketEvent(Long eventId);

PagingResponse<EventListResponseDto> getEventList(EventType type, int page, int size);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<SeatRow> 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<SeatRow> 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);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
30 changes: 15 additions & 15 deletions src/main/resources/mapper/event/EventMapper.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,20 @@

<select id="findTicketEventList" resultMap="TicketListMap">
SELECT
e.event_id AS id,
e.event_name AS name,
e.event_per_price AS perPrice,
p.performance_img AS img,
st.status_id AS statusId,
p.performance_start_date AS startDate,
p.performance_end_date AS endDate
e.event_id AS id,
e.event_name AS name,
e.event_per_price AS perPrice,
p.performance_img AS img,
st.status_id AS statusId,
p.performance_start_date AS startDate,
p.performance_end_date AS endDate
FROM event e
JOIN seat s ON e.event_id = s.event_id
JOIN performance p ON s.performance_id = p.performance_id
JOIN status st ON e.status_id = st.status_id
WHERE p.performance_end_date &gt;= NOW()
JOIN seat s ON s.seat_id = e.seat_id
JOIN performance p ON p.performance_id = s.performance_id
JOIN status st ON st.status_id = e.status_id
WHERE p.performance_end_date >= NOW()
ORDER BY e.event_created_at
LIMIT #{size} OFFSET #{offset}
LIMIT #{size} OFFSET #{offset}
</select>

<select id="countCouponEvents" resultType="long">
Expand All @@ -73,9 +73,9 @@
<select id="countTicketEvents" resultType="int">
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 &lt;= NOW()
AND p.performance_end_date &gt;= NOW()
Expand Down
Loading