Skip to content

Commit 9002bb4

Browse files
feat: 대기열 ENTERED인 상태의 유저 뒤로 보내는 API 추가
* feat: 대기열 뒤로 보내기 Service 코드 구현 * feat: 대기열 뒤로 보내기 ServController드 구현 * feat: 로직 디테일 수정 * feat: testcode 작성 * refactor: 코딩 컨벤션
1 parent 8b78091 commit 9002bb4

9 files changed

Lines changed: 447 additions & 80 deletions

File tree

backend/src/main/java/com/back/api/queue/controller/AdminQueueEntryController.java

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@
2323
import lombok.RequiredArgsConstructor;
2424

2525
@RestController
26-
@RequestMapping("/api/v1/admin/queues")
26+
@RequestMapping("/api/v1/admin/queues/{eventId}")
2727
@RequiredArgsConstructor
28-
//@PreAuthorize("hasRole('ADMIN')")
28+
@PreAuthorize("hasRole('ADMIN')")
2929
public class AdminQueueEntryController implements AdminQueueEntryApi {
3030

3131
private final QueueShuffleService queueShuffleService;
@@ -34,8 +34,7 @@ public class AdminQueueEntryController implements AdminQueueEntryApi {
3434
private final QueueEntryRedisRepository queueEntryRedisRepository;
3535

3636
@Override
37-
@PostMapping("/{eventId}/shuffle")
38-
@PreAuthorize("hasRole('ADMIN')")
37+
@PostMapping("/shuffle")
3938
public ApiResponse<ShuffleQueueResponse> shuffleQueue(
4039
@PathVariable Long eventId,
4140
@RequestBody @Valid ShuffleQueueRequest request
@@ -50,8 +49,7 @@ public ApiResponse<ShuffleQueueResponse> shuffleQueue(
5049
}
5150

5251
@Override
53-
@GetMapping("/{eventId}/statistics")
54-
@PreAuthorize("hasRole('ADMIN')")
52+
@GetMapping("/statistics")
5553
public ApiResponse<QueueStatisticsResponse> getQueueStatistics(
5654
@PathVariable Long eventId
5755
) {
@@ -61,8 +59,7 @@ public ApiResponse<QueueStatisticsResponse> getQueueStatistics(
6159

6260
//테스트용
6361
@Override
64-
@PostMapping("/{eventId}/users/{userId}/complete")
65-
@PreAuthorize("hasRole('ADMIN')")
62+
@PostMapping("/users/{userId}/complete")
6663
public ApiResponse<CompletedQueueResponse> completePayment(
6764
@PathVariable Long eventId,
6865
@PathVariable Long userId
@@ -75,8 +72,7 @@ public ApiResponse<CompletedQueueResponse> completePayment(
7572
}
7673

7774
@Override
78-
@DeleteMapping("/{eventId}/reset")
79-
@PreAuthorize("hasRole('ADMIN')")
75+
@DeleteMapping("/reset")
8076
public ApiResponse<Void> resetQueue(
8177
@PathVariable Long eventId
8278
) {

backend/src/main/java/com/back/api/queue/controller/QueueEntryApi.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.springframework.web.bind.annotation.RequestBody;
55

66
import com.back.api.queue.dto.request.ProcessEntriesRequest;
7+
import com.back.api.queue.dto.response.MoveToBackResponse;
78
import com.back.api.queue.dto.response.ProcessEntriesResponse;
89
import com.back.api.queue.dto.response.QueueEntryStatusResponse;
910
import com.back.global.config.swagger.ApiErrorCode;
@@ -89,4 +90,18 @@ ApiResponse<ProcessEntriesResponse> processIncludingMe(
8990
@Parameter(description = "이벤트 ID", example = "1")
9091
@PathVariable Long eventId
9192
);
93+
94+
95+
@Operation(
96+
summary = "사용자 대기열 맨 뒤로 이동",
97+
description = "의도적으로 퇴장한 유저의 순번을 맨 뒤로 이동시킵니다."
98+
)
99+
@ApiErrorCode({
100+
"NOT_FOUND_QUEUE_ENTRY",
101+
"NOT_ENTERED_STATUS"
102+
})
103+
ApiResponse<MoveToBackResponse> moveToBack(
104+
@Parameter(description = "이벤트 ID", example = "1")
105+
@PathVariable Long eventId
106+
);
92107
}

backend/src/main/java/com/back/api/queue/controller/QueueEntryController.java

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.springframework.web.bind.annotation.RestController;
99

1010
import com.back.api.queue.dto.request.ProcessEntriesRequest;
11+
import com.back.api.queue.dto.response.MoveToBackResponse;
1112
import com.back.api.queue.dto.response.ProcessEntriesResponse;
1213
import com.back.api.queue.dto.response.QueueEntryStatusResponse;
1314
import com.back.api.queue.service.QueueEntryProcessService;
@@ -20,7 +21,7 @@
2021

2122

2223
@RestController
23-
@RequestMapping("/api/v1/queues")
24+
@RequestMapping("/api/v1/queues/{eventId}")
2425
@RequiredArgsConstructor
2526
public class QueueEntryController implements QueueEntryApi {
2627

@@ -29,7 +30,7 @@ public class QueueEntryController implements QueueEntryApi {
2930
private final QueueEntryProcessService queueEntryProcessService;
3031

3132
@Override
32-
@GetMapping("/{eventId}/status")
33+
@GetMapping("/status")
3334
public ApiResponse<QueueEntryStatusResponse> getMyQueueEntryStatus(
3435
@PathVariable Long eventId
3536
) {
@@ -40,7 +41,7 @@ public ApiResponse<QueueEntryStatusResponse> getMyQueueEntryStatus(
4041
}
4142

4243
@Override
43-
@GetMapping("/{eventId}/exists")
44+
@GetMapping("/exists")
4445
public ApiResponse<Boolean> existsInQueue(
4546
@PathVariable Long eventId
4647
) {
@@ -50,7 +51,7 @@ public ApiResponse<Boolean> existsInQueue(
5051
}
5152

5253
@Override
53-
@PostMapping("/{eventId}/process-entries")
54+
@PostMapping("/process-entries")
5455
public ApiResponse<ProcessEntriesResponse> processTopEntries(
5556
@PathVariable Long eventId,
5657
@RequestBody(required = false) @Valid ProcessEntriesRequest request
@@ -67,7 +68,7 @@ public ApiResponse<ProcessEntriesResponse> processTopEntries(
6768
}
6869

6970
@Override
70-
@PostMapping("/{eventId}/process-until-me")
71+
@PostMapping("/process-until-me")
7172
public ApiResponse<ProcessEntriesResponse> processUntilMe(
7273
@PathVariable Long eventId
7374
) {
@@ -82,7 +83,7 @@ public ApiResponse<ProcessEntriesResponse> processUntilMe(
8283
}
8384

8485
@Override
85-
@PostMapping("/{eventId}/process-include-me")
86+
@PostMapping("/process-include-me")
8687
public ApiResponse<ProcessEntriesResponse> processIncludingMe(
8788
@PathVariable Long eventId
8889
) {
@@ -96,4 +97,19 @@ public ApiResponse<ProcessEntriesResponse> processIncludingMe(
9697
return ApiResponse.ok("나를 포함한 내 앞 사용들이 모두 입장 처리가 완료되었습니다.", response);
9798
}
9899

100+
@Override
101+
@PostMapping("/move-to-back")
102+
public ApiResponse<MoveToBackResponse> moveToBack(
103+
@PathVariable Long eventId
104+
) {
105+
Long userId = httpRequestContext.getUserId();
106+
107+
MoveToBackResponse response = queueEntryProcessService.moveToBackQueue(
108+
eventId,
109+
userId
110+
);
111+
112+
return ApiResponse.ok("대기열 맨 뒤로 이동되었습니다.", response);
113+
}
114+
99115
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.back.api.queue.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
5+
@Schema(description = "대기열 맨 뒤로 이동 응답 DTO")
6+
public record MoveToBackResponse(
7+
@Schema(description = "사용자 ID", example = "1")
8+
Long userId,
9+
10+
@Schema(description = "기존 대기열 순번", example = "3")
11+
int previousRank,
12+
13+
@Schema(description = "밀린 대기열 순번", example = "5")
14+
int newRank,
15+
16+
@Schema(description = "전체 대기 인원", example = "10")
17+
int totalWaitingUsers
18+
) {
19+
public static MoveToBackResponse from(
20+
Long userId,
21+
int previousRank,
22+
int newRank,
23+
int totalWaitingUsers
24+
) {
25+
return new MoveToBackResponse(
26+
userId,
27+
previousRank,
28+
newRank,
29+
totalWaitingUsers
30+
);
31+
}
32+
}

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

Lines changed: 50 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import com.back.api.queue.dto.response.CompletedQueueResponse;
1414
import com.back.api.queue.dto.response.EnteredQueueResponse;
1515
import com.back.api.queue.dto.response.ExpiredQueueResponse;
16+
import com.back.api.queue.dto.response.MoveToBackResponse;
1617
import com.back.api.queue.dto.response.ProcessEntriesResponse;
1718
import com.back.api.queue.dto.response.WaitingQueueBatchEventResponse;
1819
import com.back.api.queue.dto.response.WaitingQueueResponse;
@@ -91,68 +92,7 @@ public void processBatchEntry(Long eventId, List<Long> userIds) {
9192
}
9293
}
9394

94-
/*
95-
Redis에만 의존. Redis + DB 로직으로 수정했으나 주석처리 보관
96-
*/
97-
// @Transactional
98-
// public void processEventQueueEntries(Event event) {
99-
//
100-
// Long eventId = event.getId();
101-
//
102-
// //대기 중인 인원 확인
103-
// Long totalWaitingCount = queueEntryRedisRepository.getTotalWaitingCount(eventId);
104-
//
105-
// if (totalWaitingCount == 0) {
106-
// return;
107-
// }
108-
//
109-
// //입장 완료된 인원 확인
110-
// Long currentEnteredCount = queueEntryRedisRepository.getTotalEnteredCount(eventId);
111-
// int maxEnteredLimit = properties.getEntry().getMaxEnteredLimit();
112-
//
113-
// //입장 가능한 인원 확인
114-
// int availableEnteredCount = maxEnteredLimit - currentEnteredCount.intValue();
115-
//
116-
// if (availableEnteredCount <= 0) {
117-
// log.info("[EventId: {}] 최대 수용 인원 도달 - 현재: {}명, 최대: {}명",
118-
// eventId, currentEnteredCount, maxEnteredLimit);
119-
// return;
120-
// }
121-
//
122-
// //한번에 입장시킬 인원
123-
// int batchSize = properties.getEntry().getBatchSize();
124-
//
125-
// // 입장 인원 선정
126-
// // 빈 자리 순차적으로 들어갈 수 있도록 함
127-
// int entryCount = Math.min(
128-
// batchSize,
129-
// Math.min(availableEnteredCount, totalWaitingCount.intValue()) // 빈 자리와 대기 인원 중 작은 값
130-
// );
131-
//
132-
// log.info("입장 처리 - eventId: {}, 대기: {}명, 입장완료: {}명, 빈자리: {}명, 배치사이즈: {}명, 입장시킬인원: {}명",
133-
// eventId, totalWaitingCount, currentEnteredCount, availableEnteredCount, batchSize, entryCount);
134-
//
135-
//
136-
// //상위 N명 추출
137-
// Set<Object> topWaitingUsers = queueEntryRedisRepository.getTopWaitingUsers(eventId, entryCount);
138-
//
139-
// if (topWaitingUsers.isEmpty()) {
140-
// return;
141-
// }
142-
//
143-
// List<Long> userIds = new ArrayList<>();
144-
// for (Object userId : topWaitingUsers) {
145-
// userIds.add(Long.parseLong(userId.toString()));
146-
// }
147-
//
148-
// processBatchEntry(eventId, userIds); // 입장 순서인 사용자 입장처리
149-
//
150-
// publishWaitingUpdateEvents(eventId); // 대기중인 사용자 실시간 순위 업데이트
151-
//
152-
// }
153-
15495
/* ==================== 이벤트 단위 자동 입장 처리 (스케줄러) ==================== */
155-
// Redis + DB
15696
@Transactional
15797
public void processEventQueueEntries(Event event) {
15898

@@ -165,7 +105,10 @@ public void processEventQueueEntries(Event event) {
165105
totalWaitingCount = queueEntryRedisRepository.getTotalWaitingCount(eventId);
166106
} catch (Exception e) {
167107
log.warn("Redis 조회 실패, DB로부터 대기 중 인원 수 조회 시도 - eventId: {}", eventId, e);
168-
totalWaitingCount = queueEntryRepository.countByEvent_IdAndQueueEntryStatus(eventId, QueueEntryStatus.WAITING);
108+
totalWaitingCount = queueEntryRepository.countByEvent_IdAndQueueEntryStatus(
109+
eventId,
110+
QueueEntryStatus.WAITING
111+
);
169112
}
170113

171114
if (totalWaitingCount == 0) {
@@ -179,7 +122,10 @@ public void processEventQueueEntries(Event event) {
179122
currentEnteredCount = queueEntryRedisRepository.getTotalEnteredCount(eventId);
180123
} catch (Exception e) {
181124
log.warn("Redis 조회 실패, DB로부터 입장 완료된 인원 수 조회 시도 - eventId: {}", eventId, e);
182-
currentEnteredCount = queueEntryRepository.countByEvent_IdAndQueueEntryStatus(eventId, QueueEntryStatus.ENTERED);
125+
currentEnteredCount = queueEntryRepository.countByEvent_IdAndQueueEntryStatus(
126+
eventId,
127+
QueueEntryStatus.ENTERED
128+
);
183129
}
184130

185131

@@ -483,6 +429,47 @@ public void publishWaitingUpdateEvents(Long eventId) {
483429
}
484430
}
485431

432+
/* ==================== 대기열 순번 뒤로 보내기 ==================== */
433+
@Transactional
434+
public MoveToBackResponse moveToBackQueue(Long eventId, Long userId) {
435+
436+
QueueEntry queueEntry = queueEntryRepository.findByEvent_IdAndUser_Id(eventId, userId)
437+
.orElseThrow(() -> new ErrorException(QueueEntryErrorCode.NOT_FOUND_QUEUE_ENTRY));
438+
439+
// ENTERED 상태에서만 뒤로 보낸다.
440+
if (queueEntry.getQueueEntryStatus() != QueueEntryStatus.ENTERED) {
441+
throw new ErrorException(QueueEntryErrorCode.NOT_ENTERED_STATUS);
442+
}
443+
444+
int previousRank = queueEntry.getQueueRank();
445+
446+
Long maxRank = queueEntryRepository.findMaxRankInQueue(eventId)
447+
.orElse(0L);
448+
449+
int newRank = maxRank.intValue() + 1;
450+
451+
queueEntry.backToWaiting();
452+
queueEntry.updateRank(newRank);
453+
queueEntryRepository.save(queueEntry);
454+
455+
try {
456+
queueEntryRedisRepository.removeFromEnteredQueue(eventId, userId);
457+
queueEntryRedisRepository.addToWaitingQueue(eventId, userId, newRank);
458+
} catch (Exception e) {
459+
log.error("뒤로 보내기 Redis 업데이트 실패 - eventId: {}, userId: {}", eventId, userId, e);
460+
}
461+
462+
publishWaitingUpdateEvents(eventId);
463+
464+
long totalWaiting = queueEntryRepository.countByEvent_IdAndQueueEntryStatus(
465+
eventId,
466+
QueueEntryStatus.WAITING
467+
);
468+
469+
return MoveToBackResponse.from(userId, previousRank, newRank, (int)totalWaiting);
470+
471+
}
472+
486473

487474
private void validateEntry(QueueEntry queueEntry) {
488475
QueueEntryStatus status = queueEntry.getQueueEntryStatus();

backend/src/main/java/com/back/domain/queue/entity/QueueEntry.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,17 @@ public boolean isExpired() {
9191
return LocalDateTime.now().isAfter(this.expiredAt);
9292
}
9393

94+
// 사용자 입장 완료 상태 -> 대기 상태로 변경
95+
public void backToWaiting() {
96+
this.queueEntryStatus = QueueEntryStatus.WAITING;
97+
this.enteredAt = null;
98+
this.expiredAt = null;
99+
}
100+
101+
public void updateRank(int newRank) {
102+
this.queueRank = newRank;
103+
}
104+
94105
public Long getUserId() {
95106
return user.getId();
96107
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,11 @@ List<Long> findTopNWaitingUsers(
4343
@Param("count") int count
4444
);
4545

46+
@Query("SELECT MAX(q.queueRank) FROM QueueEntry q "
47+
+ "WHERE q.event.id = :eventId "
48+
)
49+
Optional<Long> findMaxRankInQueue(
50+
@Param("eventId") Long eventId
51+
);
52+
4653
}

0 commit comments

Comments
 (0)