Skip to content

Commit e97d809

Browse files
kmchaejinpokerbearkrjiyun-im-devcrocusia
authored
feat : 좌석 선택 api에 대기열 기능 연결 (#34)
Co-authored-by: pokerbearkr <ogdongwon@gmail.com> Co-authored-by: jiyun-im-dev <jiyun.im.dev@gmail.com> Co-authored-by: crocusia <132359536+crocusia@users.noreply.github.com>
1 parent 508896d commit e97d809

File tree

6 files changed

+275
-133
lines changed

6 files changed

+275
-133
lines changed

src/main/java/org/example/siljeun/domain/reservation/exception/ErrorCode.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ public enum ErrorCode {
3232

3333
// queue
3434
QUEUE_INSERT_FAIL(500, "대기열 등록을 실패했습니다."),
35-
NOT_TICKETING_TIME(400, "예매 가능 시간이 아닙니다.");
35+
NOT_TICKETING_TIME(400, "예매 가능 시간이 아닙니다."),
36+
PRECONDITION_REQUIRED(400, "선행 조건이 수행되지 않았습니다.");
3637

3738
private HttpStatus code;
3839
private String message;

src/main/java/org/example/siljeun/domain/reservation/scheduler/CheckExpiredScheduler.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,9 @@ public class CheckExpiredScheduler {
2727

2828
private final Set<String> keys = new HashSet<>();
2929

30-
// 1시간마다 티켓팅 기간인 schedule을 keys에 저장
3130
@Scheduled(cron = "0 0 * * * *")
3231
public void checkOpenedSchedule() {
3332

34-
keys.clear();
35-
3633
List<Long> openedSchedules = scheduleRepository.findAllByStartTimeAfterAndTicketingStartTimeBefore(
3734
LocalDateTime.now(),
3835
LocalDateTime.now()).stream()

src/main/java/org/example/siljeun/domain/reservation/service/WaitingQueueService.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,12 @@ public void sendWaitingNumber(String key, String username, Long scheduleId) {
110110
String destination = "/topic/queue/" + scheduleId + "/" + username;
111111
MyQueueInfoResponse response = new MyQueueInfoResponse(scheduleId, username, rank,
112112
true);
113-
messagingTemplate.convertAndSend(destination, response);
114113

115114
addSelectingQueue(scheduleId, username);
116115
deleteWaitingUser(scheduleId, username);
117116

117+
messagingTemplate.convertAndSend(destination, response);
118+
118119
return;
119120
}
120121

Lines changed: 142 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,173 +1,192 @@
11
package org.example.siljeun.domain.seatscheduleinfo.service;
22

3-
import jakarta.persistence.EntityNotFoundException;
3+
import java.time.Duration;
4+
import java.time.LocalDateTime;
5+
import java.util.ArrayList;
6+
import java.util.HashMap;
7+
import java.util.List;
8+
import java.util.Map;
49
import lombok.RequiredArgsConstructor;
510
import lombok.extern.slf4j.Slf4j;
611
import org.example.siljeun.domain.reservation.exception.CustomException;
712
import org.example.siljeun.domain.reservation.exception.ErrorCode;
13+
import org.example.siljeun.domain.reservation.service.WaitingQueueService;
814
import org.example.siljeun.domain.schedule.entity.Schedule;
915
import org.example.siljeun.domain.schedule.repository.ScheduleRepository;
10-
import org.example.siljeun.domain.seat.entity.Seat;
11-
import org.example.siljeun.domain.seat.repository.SeatRepository;
12-
import org.example.siljeun.domain.seatscheduleinfo.repository.SeatScheduleInfoRepository;
13-
import org.example.siljeun.domain.seatscheduleinfo.entity.SeatScheduleInfo;
1416
import org.example.siljeun.domain.seat.enums.SeatStatus;
17+
import org.example.siljeun.domain.seatscheduleinfo.entity.SeatScheduleInfo;
18+
import org.example.siljeun.domain.seatscheduleinfo.repository.SeatScheduleInfoRepository;
19+
import org.example.siljeun.domain.user.entity.User;
20+
import org.example.siljeun.domain.user.repository.UserRepository;
1521
import org.example.siljeun.global.lock.DistributedLock;
1622
import org.example.siljeun.global.util.RedisKeyProvider;
1723
import org.springframework.data.redis.core.RedisTemplate;
18-
import org.springframework.http.HttpStatus;
1924
import org.springframework.stereotype.Service;
2025
import org.springframework.transaction.annotation.Transactional;
21-
import org.springframework.web.server.ResponseStatusException;
22-
23-
import java.time.Duration;
24-
import java.time.LocalDateTime;
25-
import java.util.*;
2626

2727
@Slf4j
2828
@Service
2929
@RequiredArgsConstructor
3030
public class SeatScheduleInfoService {
31-
private final SeatScheduleInfoRepository seatScheduleInfoRepository;
32-
private final ScheduleRepository scheduleRepository;
33-
private final RedisTemplate<String, String> redisTemplate;
34-
35-
@DistributedLock(key = "'seat:' + #seatScheduleInfoId")
36-
public void selectSeat(Long userId, Long scheduleId, Long seatScheduleInfoId) {
3731

38-
//예외 상황 처리
39-
SeatScheduleInfo seatScheduleInfo = seatScheduleInfoRepository.findById(seatScheduleInfoId).
40-
orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUNT_SEAT_SCHEDULE_INFO));
32+
private final SeatScheduleInfoRepository seatScheduleInfoRepository;
33+
private final ScheduleRepository scheduleRepository;
34+
private final RedisTemplate<String, String> redisTemplate;
35+
private final WaitingQueueService waitingQueueService;
36+
private final UserRepository userRepository;
4137

42-
Schedule schedule = seatScheduleInfo.getSchedule();
43-
if(schedule.getTicketingStartTime().isAfter(LocalDateTime.now())){
44-
throw new CustomException(ErrorCode.NOT_TICKETING_TIME);
45-
}
38+
@DistributedLock(key = "'seat:' + #seatScheduleInfoId")
39+
public void selectSeat(Long userId, Long scheduleId, Long seatScheduleInfoId) {
4640

47-
if (!seatScheduleInfo.isAvailable()) {
48-
throw new CustomException(ErrorCode.ALREADY_SELECTED_SEAT);
49-
}
41+
User user = userRepository.findById(userId)
42+
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_USER));
5043

51-
String redisSelectedKey = RedisKeyProvider.userSelectedSeatKey(userId, scheduleId);
52-
if (Boolean.TRUE.equals(redisTemplate.hasKey(redisSelectedKey))) {
53-
throw new CustomException(ErrorCode.SEAT_LIMIT_ONE_PER_USER);
54-
}
55-
56-
//DB 상태 변경
57-
seatScheduleInfo.updateSeatScheduleInfoStatus(SeatStatus.SELECTED);
58-
seatScheduleInfoRepository.save(seatScheduleInfo);
44+
//대기열을 거쳐서 요청했는지 검증 (정상적인 요청인지 검증)
45+
boolean hasPassed = waitingQueueService.hasPassedWaitingQueue(scheduleId, user.getUsername());
46+
if (!hasPassed) {
47+
throw new CustomException(ErrorCode.PRECONDITION_REQUIRED);
48+
}
5949

60-
//유저가 선점한 좌석을 Redis에 저장 (정보 조회용)
61-
redisTemplate.opsForValue()
62-
.set(redisSelectedKey, seatScheduleInfoId.toString());
63-
redisTemplate.expire(redisSelectedKey, Duration.ofMinutes(5));
50+
//예외 상황 처리
51+
SeatScheduleInfo seatScheduleInfo = seatScheduleInfoRepository.findById(seatScheduleInfoId).
52+
orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUNT_SEAT_SCHEDULE_INFO));
6453

65-
//TTL 관리를 위한 키 생성
66-
String redisLockKey = RedisKeyProvider.seatOccupyKey(seatScheduleInfoId);
67-
redisTemplate.opsForValue().set(redisLockKey, userId.toString());
54+
Schedule schedule = seatScheduleInfo.getSchedule();
55+
if (schedule.getTicketingStartTime().isAfter(LocalDateTime.now())) {
56+
throw new CustomException(ErrorCode.NOT_TICKETING_TIME);
57+
}
6858

69-
//Redis 상태 변경
70-
updateSeatScheduleInfoStatusInRedis(scheduleId, seatScheduleInfoId, SeatStatus.SELECTED);
59+
if (!seatScheduleInfo.isAvailable()) {
60+
throw new CustomException(ErrorCode.ALREADY_SELECTED_SEAT);
61+
}
7162

72-
//TTL 적용
73-
applySeatLockTTL(seatScheduleInfoId, SeatStatus.SELECTED);
63+
String redisSelectedKey = RedisKeyProvider.userSelectedSeatKey(userId, scheduleId);
64+
if (Boolean.TRUE.equals(redisTemplate.hasKey(redisSelectedKey))) {
65+
throw new CustomException(ErrorCode.SEAT_LIMIT_ONE_PER_USER);
7466
}
7567

76-
public Map<String, String> getSeatStatusMap(Long scheduleId) {
68+
//DB 상태 변경
69+
seatScheduleInfo.updateSeatScheduleInfoStatus(SeatStatus.SELECTED);
70+
seatScheduleInfoRepository.save(seatScheduleInfo);
7771

78-
Schedule schedule = scheduleRepository.findById(scheduleId)
79-
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_SCHEDULE));
72+
//유저가 선점한 좌석을 Redis에 저장 (정보 조회용)
73+
redisTemplate.opsForValue()
74+
.set(redisSelectedKey, seatScheduleInfoId.toString());
75+
redisTemplate.expire(redisSelectedKey, Duration.ofMinutes(5));
8076

81-
List<SeatScheduleInfo> seatScheduleInfos = seatScheduleInfoRepository.findAllBySchedule(schedule);
82-
if(seatScheduleInfos.isEmpty()){
83-
throw new CustomException(ErrorCode.NOT_FOUNT_SEAT_SCHEDULE_INFO);
84-
}
77+
//TTL 관리를 위한 키 생성
78+
String redisLockKey = RedisKeyProvider.seatOccupyKey(seatScheduleInfoId);
79+
redisTemplate.opsForValue().set(redisLockKey, userId.toString());
8580

86-
List<String> fieldKeys = seatScheduleInfos.stream()
87-
.map(info -> info.getId().toString())
88-
.toList();
81+
//Redis 상태 변경
82+
updateSeatScheduleInfoStatusInRedis(scheduleId, seatScheduleInfoId, SeatStatus.SELECTED);
8983

90-
String redisKey = RedisKeyProvider.seatStatusKey(scheduleId);
91-
List<Object> redisStatuses = redisTemplate.opsForHash().multiGet(redisKey, new ArrayList<>(fieldKeys));
84+
//TTL 적용
85+
applySeatLockTTL(seatScheduleInfoId, SeatStatus.SELECTED);
9286

93-
Map<String, String> seatStatusMap = new HashMap<>();
94-
for (int i = 0; i < seatScheduleInfos.size(); i++) {
95-
SeatScheduleInfo info = seatScheduleInfos.get(i);
96-
Object redisStatusObj = redisStatuses.get(i);
87+
//좌석 선택 queue에서 데이터 삭제
88+
waitingQueueService.addSelectingQueue(scheduleId, user.getUsername());
89+
}
9790

98-
String status = redisStatusObj != null
99-
? redisStatusObj.toString()
100-
: seatScheduleInfos.get(i).getStatus().name();
91+
public Map<String, String> getSeatStatusMap(Long scheduleId) {
10192

102-
seatStatusMap.put(info.getId().toString(), status);
103-
}
93+
Schedule schedule = scheduleRepository.findById(scheduleId)
94+
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_SCHEDULE));
10495

105-
return seatStatusMap;
96+
List<SeatScheduleInfo> seatScheduleInfos = seatScheduleInfoRepository.findAllBySchedule(
97+
schedule);
98+
if (seatScheduleInfos.isEmpty()) {
99+
throw new CustomException(ErrorCode.NOT_FOUNT_SEAT_SCHEDULE_INFO);
106100
}
107101

108-
public void forceSeatScheduleInfoInRedis(Long scheduleId){
109-
Schedule schedule = scheduleRepository.findById(scheduleId)
110-
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_SCHEDULE));
102+
List<String> fieldKeys = seatScheduleInfos.stream()
103+
.map(info -> info.getId().toString())
104+
.toList();
111105

112-
List<SeatScheduleInfo> seatInfos = seatScheduleInfoRepository.findAllBySchedule(schedule);
106+
String redisKey = RedisKeyProvider.seatStatusKey(scheduleId);
107+
List<Object> redisStatuses = redisTemplate.opsForHash()
108+
.multiGet(redisKey, new ArrayList<>(fieldKeys));
113109

114-
String redisHashKey = RedisKeyProvider.seatStatusKey(scheduleId);
115-
Map<String, String> seatStatusMap = new HashMap<>();
110+
Map<String, String> seatStatusMap = new HashMap<>();
111+
for (int i = 0; i < seatScheduleInfos.size(); i++) {
112+
SeatScheduleInfo info = seatScheduleInfos.get(i);
113+
Object redisStatusObj = redisStatuses.get(i);
116114

117-
for (SeatScheduleInfo seat : seatInfos) {
118-
seatStatusMap.put(seat.getId().toString(), seat.getStatus().name());
119-
}
115+
String status = redisStatusObj != null
116+
? redisStatusObj.toString()
117+
: seatScheduleInfos.get(i).getStatus().name();
120118

121-
redisTemplate.opsForHash().putAll(redisHashKey, seatStatusMap);
122-
}
123-
@Transactional
124-
public void updateSeatScheduleInfoStatus(Long seatScheduleInfoId, SeatStatus seatStatus){
125-
SeatScheduleInfo seatScheduleInfo = seatScheduleInfoRepository.findById(seatScheduleInfoId)
126-
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUNT_SEAT_SCHEDULE_INFO));
127-
seatScheduleInfo.updateSeatScheduleInfoStatus(seatStatus);
128-
129-
Long scheduleId = seatScheduleInfo.getSchedule().getId();
130-
updateSeatScheduleInfoStatusInRedis(scheduleId, seatScheduleInfoId, seatStatus);
119+
seatStatusMap.put(info.getId().toString(), status);
131120
}
132121

133-
public void updateSeatScheduleInfoStatusInRedis(Long scheduleId, Long seatScheduleInfoId, SeatStatus seatStatus){
134-
String redisKey = RedisKeyProvider.seatStatusKey(scheduleId);
135-
String fieldKey = seatScheduleInfoId.toString();
136-
redisTemplate.opsForHash().put(redisKey, fieldKey, seatStatus.name());
137-
}
122+
return seatStatusMap;
123+
}
124+
125+
public void forceSeatScheduleInfoInRedis(Long scheduleId) {
126+
Schedule schedule = scheduleRepository.findById(scheduleId)
127+
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_SCHEDULE));
128+
129+
List<SeatScheduleInfo> seatInfos = seatScheduleInfoRepository.findAllBySchedule(schedule);
138130

139-
public void applySeatLockTTL(Long seatScheduleInfoId, SeatStatus seatStatus){
140-
String member = seatScheduleInfoId.toString();
141-
142-
String seatLockkey = RedisKeyProvider.seatOccupyKey(seatScheduleInfoId);
143-
String zsetSelectedKey = RedisKeyProvider.trackExpiresKey(SeatStatus.SELECTED.name());
144-
String zsetHoldKey = RedisKeyProvider.trackExpiresKey(SeatStatus.HOLD.name());
145-
146-
Duration ttl = null;
147-
long nowMillis = System.currentTimeMillis();
148-
149-
redisTemplate.opsForZSet().remove(zsetSelectedKey, member);
150-
redisTemplate.opsForZSet().remove(zsetHoldKey, member);
151-
152-
switch(seatStatus){
153-
case SELECTED:
154-
ttl = Duration.ofMinutes(5);
155-
redisTemplate.expire(seatLockkey, ttl);
156-
redisTemplate.opsForZSet().add(zsetSelectedKey, member, nowMillis+ttl.toMillis());
157-
break;
158-
case HOLD:
159-
ttl = Duration.ofMinutes(60);
160-
redisTemplate.expire(seatLockkey, ttl);
161-
redisTemplate.opsForZSet().add(zsetHoldKey, member, nowMillis+ttl.toMillis());
162-
break;
163-
default:
164-
redisTemplate.persist(seatLockkey);
165-
break;
166-
}
131+
String redisHashKey = RedisKeyProvider.seatStatusKey(scheduleId);
132+
Map<String, String> seatStatusMap = new HashMap<>();
133+
134+
for (SeatScheduleInfo seat : seatInfos) {
135+
seatStatusMap.put(seat.getId().toString(), seat.getStatus().name());
167136
}
168137

169-
public SeatScheduleInfo findById(Long seatScheduleInfoId){
170-
return seatScheduleInfoRepository.findById(seatScheduleInfoId)
171-
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUNT_SEAT_SCHEDULE_INFO));
138+
redisTemplate.opsForHash().putAll(redisHashKey, seatStatusMap);
139+
}
140+
141+
@Transactional
142+
public void updateSeatScheduleInfoStatus(Long seatScheduleInfoId, SeatStatus seatStatus) {
143+
SeatScheduleInfo seatScheduleInfo = seatScheduleInfoRepository.findById(seatScheduleInfoId)
144+
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUNT_SEAT_SCHEDULE_INFO));
145+
seatScheduleInfo.updateSeatScheduleInfoStatus(seatStatus);
146+
147+
Long scheduleId = seatScheduleInfo.getSchedule().getId();
148+
updateSeatScheduleInfoStatusInRedis(scheduleId, seatScheduleInfoId, seatStatus);
149+
}
150+
151+
public void updateSeatScheduleInfoStatusInRedis(Long scheduleId, Long seatScheduleInfoId,
152+
SeatStatus seatStatus) {
153+
String redisKey = RedisKeyProvider.seatStatusKey(scheduleId);
154+
String fieldKey = seatScheduleInfoId.toString();
155+
redisTemplate.opsForHash().put(redisKey, fieldKey, seatStatus.name());
156+
}
157+
158+
public void applySeatLockTTL(Long seatScheduleInfoId, SeatStatus seatStatus) {
159+
String member = seatScheduleInfoId.toString();
160+
161+
String seatLockkey = RedisKeyProvider.seatOccupyKey(seatScheduleInfoId);
162+
String zsetSelectedKey = RedisKeyProvider.trackExpiresKey(SeatStatus.SELECTED.name());
163+
String zsetHoldKey = RedisKeyProvider.trackExpiresKey(SeatStatus.HOLD.name());
164+
165+
Duration ttl = null;
166+
long nowMillis = System.currentTimeMillis();
167+
168+
redisTemplate.opsForZSet().remove(zsetSelectedKey, member);
169+
redisTemplate.opsForZSet().remove(zsetHoldKey, member);
170+
171+
switch (seatStatus) {
172+
case SELECTED:
173+
ttl = Duration.ofMinutes(5);
174+
redisTemplate.expire(seatLockkey, ttl);
175+
redisTemplate.opsForZSet().add(zsetSelectedKey, member, nowMillis + ttl.toMillis());
176+
break;
177+
case HOLD:
178+
ttl = Duration.ofMinutes(60);
179+
redisTemplate.expire(seatLockkey, ttl);
180+
redisTemplate.opsForZSet().add(zsetHoldKey, member, nowMillis + ttl.toMillis());
181+
break;
182+
default:
183+
redisTemplate.persist(seatLockkey);
184+
break;
172185
}
186+
}
187+
188+
public SeatScheduleInfo findById(Long seatScheduleInfoId) {
189+
return seatScheduleInfoRepository.findById(seatScheduleInfoId)
190+
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUNT_SEAT_SCHEDULE_INFO));
191+
}
173192
}

0 commit comments

Comments
 (0)