Skip to content

Commit 4a174c3

Browse files
authored
[refactor] 사전등록 및 티켓팅한 사용자 회원탈퇴 로직 보완
1 parent d3c9af1 commit 4a174c3

12 files changed

Lines changed: 388 additions & 37 deletions

File tree

backend/src/main/java/com/back/api/queue/service/QueueEntryProcessService.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,39 @@ public void expireEntry(Long eventId, Long userId) {
342342
);
343343
}
344344

345+
@Transactional
346+
public void expireWaitingAndEnteredEntry(Long eventId, Long userId) {
347+
QueueEntry queueEntry = queueEntryRepository.findByEvent_IdAndUser_Id(eventId, userId)
348+
.orElseThrow(() -> new ErrorException(QueueEntryErrorCode.NOT_FOUND_QUEUE_ENTRY));
349+
350+
if (queueEntry.getQueueEntryStatus() == QueueEntryStatus.EXPIRED
351+
|| queueEntry.getQueueEntryStatus() == QueueEntryStatus.COMPLETED) {
352+
return;
353+
}
354+
355+
queueEntry.expire();
356+
QueueEntry deque = queueEntryRepository.save(queueEntry);
357+
358+
try {
359+
queueEntryRedisRepository.removeFromWaitingAndEnteredQueue(eventId, userId);
360+
log.debug("eventId {} - Redis 만료 처리 성공", eventId);
361+
} catch (Exception e) {
362+
log.error("eventId {} - Redis 만료 처리 실패", eventId);
363+
}
364+
365+
publishExpiredEvent(queueEntry); // 만료 처리 웹소켓 이벤트 발행
366+
367+
eventPublisher.publishEvent(
368+
new QueueExpiredMessage(
369+
userId,
370+
deque.getId(),
371+
eventRepository.findById(eventId)
372+
.map(Event::getTitle)
373+
.orElse("제목 없음")
374+
)
375+
);
376+
}
377+
345378
@Transactional
346379
public void expireBatchEntries(List<QueueEntry> entries) {
347380

backend/src/main/java/com/back/api/queue/service/QueueEntryReadService.java

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.back.api.queue.service;
22

3+
import java.util.List;
4+
35
import org.springframework.data.domain.Page;
46
import org.springframework.data.domain.PageRequest;
57
import org.springframework.data.domain.Pageable;
@@ -42,16 +44,29 @@ public class QueueEntryReadService {
4244

4345
public QueueEntryStatusResponse getMyQueueStatus(Long eventId, Long userId) {
4446
QueueEntry entry = queueEntryRepository.findByEvent_IdAndUser_Id(eventId, userId)
45-
.orElseThrow(()-> new ErrorException(QueueEntryErrorCode.NOT_FOUND_QUEUE_ENTRY));
47+
.orElseThrow(() -> new ErrorException(QueueEntryErrorCode.NOT_FOUND_QUEUE_ENTRY));
4648

47-
return switch(entry.getQueueEntryStatus()) {
49+
return switch (entry.getQueueEntryStatus()) {
4850
case WAITING -> buildWaitingQueueResponse(eventId, entry);
49-
case ENTERED -> buildEnteredQueueResponse(entry);
50-
case EXPIRED -> buildExpiredQueueResponse(entry);
51+
case ENTERED -> buildEnteredQueueResponse(entry);
52+
case EXPIRED -> buildExpiredQueueResponse(entry);
5153
case COMPLETED -> buildCompletedQueueResponse(entry);
5254
};
5355
}
5456

57+
public List<QueueEntry> getMyAllQueues(Long userId) {
58+
return queueEntryRepository.findAllByUserId(userId);
59+
}
60+
61+
public List<QueueEntry> getWaitingOrEnteredQueues(Long userId) {
62+
return getMyAllQueues(userId)
63+
.stream()
64+
.filter(queue ->
65+
queue.getQueueEntryStatus().equals(QueueEntryStatus.WAITING)
66+
|| queue.getQueueEntryStatus().equals(QueueEntryStatus.ENTERED))
67+
.toList();
68+
}
69+
5570
//Redis 조회 + 계산
5671
//단일 사용자 조회 (API에서 사용 예정)
5772
public WaitingQueueResponse buildWaitingQueueResponseForUser(Long eventId, Long userId) {
@@ -90,7 +105,7 @@ public WaitingQueueResponse buildWaitingQueueResponseFromRank(
90105
} else {
91106
estimatedWaitTime = waitingAhead * 3;
92107
progress = totalWaitingCount > 0
93-
? (int) (((totalWaitingCount - waitingAhead) * 100) / totalWaitingCount)
108+
? (int)(((totalWaitingCount - waitingAhead) * 100) / totalWaitingCount)
94109
: 0;
95110
}
96111

@@ -151,7 +166,6 @@ public QueueStatisticsResponse getQueueStatistics(Long eventId) {
151166
throw new ErrorException(QueueEntryErrorCode.NOT_FOUND_QUEUE_ENTRY);
152167
}
153168

154-
155169
long waitingCount = queueEntryRepository.countByEvent_IdAndQueueEntryStatus(
156170
eventId,
157171
QueueEntryStatus.WAITING
@@ -217,8 +231,8 @@ private WaitingQueueResponse buildWaitingQueueResponseFromDB(Long eventId, Queue
217231
entry.getUserId(),
218232
entry.getEventId(),
219233
entry.getQueueRank(),
220-
(int) waitingAheadCount,
221-
(int) totalWaitingCount
234+
(int)waitingAheadCount,
235+
(int)totalWaitingCount
222236
);
223237
}
224238

backend/src/main/java/com/back/api/ticket/service/TicketService.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.back.api.ticket.service;
22

3+
import java.time.LocalDateTime;
34
import java.util.List;
45
import java.util.Optional;
56

@@ -173,6 +174,10 @@ public List<TicketResponse> getMyTickets(Long userId) {
173174
return ticketRepository.findMyTicketDto(userId);
174175
}
175176

177+
public List<Ticket> getMyIssuedOrPaidTicketsBeforeEvent(Long userId) {
178+
return ticketRepository.findIssuedOrPaidBeforeEvent(userId, LocalDateTime.now());
179+
}
180+
176181
/**
177182
* 티켓 상세 조회
178183
*/

backend/src/main/java/com/back/api/user/service/UserService.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
package com.back.api.user.service;
22

33
import java.time.LocalDate;
4+
import java.util.List;
45

56
import org.springframework.stereotype.Service;
67
import org.springframework.transaction.annotation.Transactional;
78

9+
import com.back.api.queue.service.QueueEntryProcessService;
10+
import com.back.api.queue.service.QueueEntryReadService;
11+
import com.back.api.ticket.service.TicketService;
812
import com.back.api.auth.service.ActiveSessionCache;
913
import com.back.api.user.dto.request.UpdateProfileRequest;
1014
import com.back.api.user.dto.response.UserProfileResponse;
1115
import com.back.domain.auth.repository.RefreshTokenRepository;
16+
import com.back.domain.queue.entity.QueueEntry;
17+
import com.back.domain.ticket.entity.Ticket;
1218
import com.back.domain.user.entity.User;
1319
import com.back.domain.user.repository.UserRepository;
1420
import com.back.global.error.code.AuthErrorCode;
@@ -17,16 +23,22 @@
1723
import com.back.global.http.HttpRequestContext;
1824

1925
import lombok.RequiredArgsConstructor;
26+
import lombok.extern.slf4j.Slf4j;
2027

2128
@Service
2229
@RequiredArgsConstructor
30+
@Slf4j
2331
public class UserService {
2432

2533
private final UserRepository userRepository;
2634
private final RefreshTokenRepository refreshTokenRepository;
2735
private final ActiveSessionCache activeSessionCache;
2836
private final HttpRequestContext requestContext;
37+
private final QueueEntryReadService queueEntryReadService;
38+
private final QueueEntryProcessService queueEntryProcessService;
39+
private final TicketService ticketService;
2940

41+
@Transactional
3042
public UserProfileResponse getUser(Long userId) {
3143
User user = userRepository.findById(userId)
3244
.orElseThrow(() -> new ErrorException(UserErrorCode.NOT_FOUND));
@@ -59,6 +71,8 @@ public void deleteUser(long userId) {
5971
User user = userRepository.findById(userId)
6072
.orElseThrow(() -> new ErrorException(UserErrorCode.NOT_FOUND));
6173

74+
handleDeleteUserQueue(userId);
75+
6276
// 모든 기기의 refreshToken 무효화 (전 기기 로그아웃 효과)
6377
refreshTokenRepository.revokeAllByUserId(userId);
6478

@@ -70,4 +84,32 @@ public void deleteUser(long userId) {
7084

7185
user.softDelete();
7286
}
87+
88+
private void handleDeleteUserQueue(long userId) {
89+
// 1. 사전등록 인원: 랜덤 큐 스케줄러 동작 전까지 회원탈퇴 가능
90+
List<QueueEntry> waitingOrEnteredQueues = queueEntryReadService.getWaitingOrEnteredQueues(userId);
91+
92+
// 2. 티켓을 구매한 상태면 행사 오픈날짜가 지나고 회원탈퇴 할 수 있도록 하기
93+
List<Ticket> tickets = ticketService.getMyIssuedOrPaidTicketsBeforeEvent(userId);
94+
95+
if (waitingOrEnteredQueues.isEmpty() && tickets.isEmpty()) {
96+
return;
97+
}
98+
99+
// 랜덤 큐에 배정된 이후회원 탈퇴를 시도하면, 사용자 삭제: 배정된 랜덤큐에서 해당 사옹자 제거
100+
if (!waitingOrEnteredQueues.isEmpty() && tickets.isEmpty()) {
101+
removeUserInQueues(waitingOrEnteredQueues, userId);
102+
log.info("Success remove WAITING|ENTERED queues for deleted userId: {}", userId);
103+
return;
104+
}
105+
106+
log.error("Can't delete user for userId: {}", userId);
107+
throw new ErrorException(UserErrorCode.CAN_NOT_DELETE_USER);
108+
}
109+
110+
private void removeUserInQueues(List<QueueEntry> queues, long userId) {
111+
for (QueueEntry queue : queues) {
112+
queueEntryProcessService.expireWaitingAndEnteredEntry(queue.getEventId(), userId);
113+
}
114+
}
73115
}

backend/src/main/java/com/back/domain/queue/repository/QueueEntryRedisRepository.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,25 @@ public void removeFromEnteredQueue(Long eventId, Long userId) {
9292
log.info("Removed user from entered queue - eventId: {}, userId: {}", eventId, userId);
9393
}
9494

95+
public void removeFromWaitingAndEnteredQueue(Long eventId, Long userId) {
96+
// WAITING 제거
97+
String waitingKey = String.format(WAITING_KEY, eventId);
98+
redisTemplate.opsForZSet().remove(waitingKey, userId.toString());
99+
100+
// ENTERED 제거
101+
String enteredKey = String.format(ENTERED_KEY, eventId);
102+
redisTemplate.opsForSet().remove(enteredKey, userId.toString());
103+
104+
log.info("Removed user from waiting & entered queue - eventId: {}, userId: {}", eventId, userId);
105+
}
106+
95107
public Long getTotalEnteredCount(Long eventId) {
96108
String key = String.format(ENTERED_KEY, eventId);
97109
Long size = redisTemplate.opsForSet().size(key);
98110
return size != null ? size : 0L;
99111
}
100112

101-
public boolean isInEnteredQueue(Long eventId, Long userId) {
113+
public boolean isInEnteredQueue(Long eventId, Long userId) {
102114
String key = String.format(ENTERED_KEY, eventId);
103115
Boolean isMember = redisTemplate.opsForSet().isMember(key, userId.toString());
104116
return isMember != null && isMember;
@@ -116,7 +128,6 @@ public Long getEnteredCount(Long eventId) {
116128
return count != null ? (Long)count : 0L;
117129
}
118130

119-
120131
/* ==================== 임시 데이터 추가용 ==================== */
121132
public void addToEnteredQueueDirectly(Long eventId, Long userId) {
122133
String key = String.format(ENTERED_KEY, eventId);

backend/src/main/java/com/back/domain/queue/repository/QueueEntryRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ Optional<Long> findMaxRankInQueue(
5252
@Param("eventId") Long eventId
5353
);
5454

55+
@Query("SELECT q FROM QueueEntry q WHERE q.user.id = :userId")
56+
List<QueueEntry> findAllByUserId(@Param("userId") Long userId);
57+
5558
@Query("SELECT q FROM QueueEntry q "
5659
+ "JOIN FETCH q.user u "
5760
+ "JOIN FETCH q.event e "
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package com.back.domain.ticket.repository;
22

3+
import java.time.LocalDateTime;
34
import java.util.List;
45

56
import com.back.api.ticket.dto.response.TicketResponse;
7+
import com.back.domain.ticket.entity.Ticket;
68

79
public interface TicketRepositoryCustom {
810

911
List<TicketResponse> findMyTicketDto(Long userId);
1012

11-
}
13+
List<Ticket> findIssuedOrPaidBeforeEvent(Long userId, LocalDateTime now);
14+
}

backend/src/main/java/com/back/domain/ticket/repository/TicketRepositoryImpl.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
import static com.back.domain.seat.entity.QSeat.*;
55
import static com.back.domain.ticket.entity.QTicket.*;
66

7+
import java.time.LocalDateTime;
78
import java.util.List;
89

910
import org.springframework.stereotype.Repository;
1011

1112
import com.back.api.ticket.dto.response.TicketResponse;
13+
import com.back.domain.ticket.entity.Ticket;
1214
import com.back.domain.ticket.entity.TicketStatus;
1315
import com.querydsl.core.types.Projections;
1416
import com.querydsl.jpa.impl.JPAQueryFactory;
@@ -52,4 +54,17 @@ public List<TicketResponse> findMyTicketDto(Long userId) {
5254
.fetch();
5355

5456
}
57+
58+
@Override
59+
public List<Ticket> findIssuedOrPaidBeforeEvent(Long userId, LocalDateTime now) {
60+
return jpaQueryFactory
61+
.selectFrom(ticket)
62+
.join(ticket.event, event).fetchJoin()
63+
.where(
64+
ticket.owner.id.eq(userId),
65+
ticket.ticketStatus.in(TicketStatus.ISSUED, TicketStatus.PAID),
66+
event.eventDate.after(now)
67+
)
68+
.fetch();
69+
}
5570
}

backend/src/main/java/com/back/global/error/code/UserErrorCode.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
@Getter
99
@AllArgsConstructor
1010
public enum UserErrorCode implements ErrorCode {
11-
NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다.");
11+
NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."),
12+
CAN_NOT_DELETE_USER(HttpStatus.BAD_REQUEST, "신청하신 행사가 종료된 후에 탈퇴 할 수 있습니다.");
1213

1314
private final HttpStatus httpStatus;
1415
private final String message;

0 commit comments

Comments
 (0)