Skip to content

Commit 31bc7f8

Browse files
committed
✨ feat: 예약 마감 로직 구현
- 예약 마감 실행을 스케줄링 - 예약 진행과 관련된 모든 상태 초기화 Issue Resolved: #243
1 parent 6a8c578 commit 31bc7f8

File tree

6 files changed

+177
-29
lines changed

6 files changed

+177
-29
lines changed

back/src/domains/booking/service/booking-seats.service.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import Redis from 'ioredis';
1010
import { BehaviorSubject } from 'rxjs';
1111
import { map } from 'rxjs/operators';
1212

13-
import { AuthService } from '../../../auth/service/auth.service';
1413
import { UserService } from '../../user/service/user.service';
1514
import { SEATS_BROADCAST_INTERVAL } from '../const/seatsBroadcastInterval.const';
1615
import { SEATS_SSE_RETRY_TIME } from '../const/seatsSseRetryTime.const';
@@ -36,7 +35,6 @@ export class BookingSeatsService {
3635
constructor(
3736
private redisService: RedisService,
3837
private inBookingService: InBookingService,
39-
private authService: AuthService,
4038
private eventEmitter: EventEmitter2,
4139
private readonly userService: UserService,
4240
) {
@@ -58,6 +56,18 @@ export class BookingSeatsService {
5856
this.seatsSubscriptionMap.set(eventId, subscription);
5957
}
6058

59+
async clearSeatsSubscription(eventId: number) {
60+
const subscription = this.seatsSubscriptionMap.get(eventId);
61+
if (subscription) {
62+
subscription.complete();
63+
this.seatsSubscriptionMap.delete(eventId);
64+
}
65+
const keys = await this.redis.keys(`event:${eventId}:*`);
66+
if (keys.length > 0) {
67+
await this.redis.unlink(...keys);
68+
}
69+
}
70+
6171
async bookSeat(sid: string, target: [number, number]) {
6272
const eventId = await this.userService.getUserEventTarget(sid);
6373
const bookedSeat = await this.inBookingService.getBookedSeats(sid);

back/src/domains/booking/service/booking.service.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { OnEvent } from '@nestjs/event-emitter';
44
import Redis from 'ioredis';
55

66
import { AuthService } from '../../../auth/service/auth.service';
7-
import { EventService } from '../../event/service/event.service';
87
import { UserService } from '../../user/service/user.service';
98
import { BookingAdmissionStatusDto } from '../dto/bookingAdmissionStatus.dto';
109
import { ServerTimeDto } from '../dto/serverTime.dto';
@@ -21,7 +20,6 @@ export class BookingService {
2120
private readonly redis: Redis | null;
2221
constructor(
2322
private readonly redisService: RedisService,
24-
private readonly eventService: EventService,
2523
private readonly authService: AuthService,
2624
private readonly bookingSeatsService: BookingSeatsService,
2725
private readonly inBookingService: InBookingService,
@@ -37,9 +35,11 @@ export class BookingService {
3735
async onSeatsSseDisconnected(event: { sid: string }) {
3836
const sid = event.sid;
3937
const eventId = await this.userService.getUserEventTarget(sid);
40-
await this.collectSeatsIfNotSaved(eventId, sid);
41-
await this.inBookingService.emitSession(sid);
42-
await this.letInNextWaiting(eventId);
38+
if (await this.openBookingService.isEventOpened(eventId)) {
39+
await this.collectSeatsIfNotSaved(eventId, sid);
40+
await this.inBookingService.emitSession(sid);
41+
await this.letInNextWaiting(eventId);
42+
}
4343
}
4444

4545
private async collectSeatsIfNotSaved(eventId: number, sid: string) {
@@ -98,18 +98,9 @@ export class BookingService {
9898

9999
// 함수 이름 생각하기
100100
async isAdmission(eventId: number, sid: string): Promise<BookingAdmissionStatusDto> {
101-
// eventId를 받아서 해당 이벤트가 존재하는지 확인한다.
102-
const event = await this.eventService.findEvent({ eventId });
103-
const now = new Date();
104-
105101
const isOpened = await this.openBookingService.isEventOpened(eventId);
106-
107102
if (!isOpened) {
108-
throw new BadRequestException('아직 예약이 오픈되지 않았습니다.');
109-
} else if (now >= event.reservationCloseDate) {
110-
//event 시간 확인 이벤트 종료시간 이후인지
111-
// 예약 시간이 아닙니다.
112-
throw new BadRequestException('이미 예약 마감된 이벤트입니다.');
103+
throw new BadRequestException('예약이 오픈되지 않았습니다.');
113104
}
114105

115106
await this.userService.setUserEventTarget(sid, eventId);

back/src/domains/booking/service/enter-booking.service.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { RedisService } from '@liaoliaots/nestjs-redis';
22
import { Injectable } from '@nestjs/common';
33
import { EventEmitter2 } from '@nestjs/event-emitter';
4+
import { SchedulerRegistry } from '@nestjs/schedule';
45
import Redis from 'ioredis';
56

67
import { UserService } from '../../user/service/user.service';
@@ -13,15 +14,30 @@ export class EnterBookingService {
1314
private readonly redisService: RedisService,
1415
private readonly userService: UserService,
1516
private readonly eventEmitter: EventEmitter2,
17+
private readonly schedulerRegistry: SchedulerRegistry,
1618
) {
1719
this.redis = this.redisService.getOrThrow();
1820
}
1921

2022
async gcEnteringSessions(eventId: number) {
21-
setInterval(() => {
23+
this.deleteIntervalIfExists(`gc-entering-${eventId}`);
24+
25+
const interval = setInterval(() => {
2226
this.removeExpiredSessions(eventId);
2327
this.eventEmitter.emit('entering-sessions-gc', { eventId });
2428
}, ENTERING_GC_INTERVAL);
29+
30+
this.schedulerRegistry.addInterval(`gc-entering-${eventId}`, interval);
31+
}
32+
33+
clearGCInterval(eventId: number) {
34+
this.deleteIntervalIfExists(`gc-entering-${eventId}`);
35+
}
36+
37+
private deleteIntervalIfExists(intervalName: string) {
38+
if (this.schedulerRegistry.doesExist('interval', intervalName)) {
39+
this.schedulerRegistry.deleteInterval(intervalName);
40+
}
2541
}
2642

2743
async addEnteringSession(sid: string) {
@@ -64,4 +80,16 @@ export class EnterBookingService {
6480
const expiryTimestamp = Date.now() - ENTERING_SESSION_EXPIRY;
6581
await this.redis.zremrangebyscore(`entering:${eventId}`, 0, expiryTimestamp);
6682
}
83+
84+
async getAllEnteringSids(eventId: number) {
85+
return this.redis.zrange(`entering:${eventId}`, 0, -1);
86+
}
87+
88+
async clearEnteringPool(eventId: number) {
89+
this.clearGCInterval(eventId);
90+
const keys = await this.redis.keys('entering:*');
91+
if (keys.length > 0) {
92+
await this.redis.unlink(...keys);
93+
}
94+
}
6795
}

back/src/domains/booking/service/in-booking.service.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,15 @@ export class InBookingService {
179179
bookedSeats: session.bookedSeats,
180180
};
181181
}
182+
183+
async getAllInBookingSids(eventId: number) {
184+
return this.redis.smembers(this.getEventKey(eventId));
185+
}
186+
187+
async clearInBookingPool(eventId: number) {
188+
const keys = await this.redis.keys(`in-booking:${eventId}:*`);
189+
if (keys.length > 0) {
190+
await this.redis.unlink(...keys);
191+
}
192+
}
182193
}

back/src/domains/booking/service/open-booking.service.ts

Lines changed: 94 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@ import { Cron, SchedulerRegistry } from '@nestjs/schedule';
44
import { CronJob } from 'cron';
55
import Redis from 'ioredis';
66

7+
import { AuthService } from '../../../auth/service/auth.service';
78
import { Event } from '../../event/entity/event.entity';
89
import { EventRepository } from '../../event/repository/event.reposiotry';
910
import { SectionRepository } from '../../place/repository/section.repository';
11+
import { UserService } from '../../user/service/user.service';
1012
import { ONE_MINUTE_BEFORE_THE_HOUR } from '../const/cronExpressions.const';
1113
import { IN_BOOKING_DEFAULT_MAX_SIZE } from '../const/inBookingDefaultMaxSize.const';
1214

1315
import { BookingSeatsService } from './booking-seats.service';
1416
import { EnterBookingService } from './enter-booking.service';
1517
import { InBookingService } from './in-booking.service';
18+
import { WaitingQueueService } from './waiting-queue.service';
1619

1720
@Injectable()
1821
export class OpenBookingService implements OnApplicationBootstrap {
@@ -22,8 +25,11 @@ export class OpenBookingService implements OnApplicationBootstrap {
2225
private redisService: RedisService,
2326
private eventRepository: EventRepository,
2427
private sectionRepository: SectionRepository,
28+
private authService: AuthService,
29+
private userService: UserService,
2530
private inBookingService: InBookingService,
2631
private seatsUpdateService: BookingSeatsService,
32+
private waitingQueueService: WaitingQueueService,
2733
private enterBookingService: EnterBookingService,
2834
private schedulerRegistry: SchedulerRegistry,
2935
) {
@@ -38,21 +44,35 @@ export class OpenBookingService implements OnApplicationBootstrap {
3844
@Cron(ONE_MINUTE_BEFORE_THE_HOUR)
3945
async scheduleUpcomingReservations() {
4046
const comingEvents = await this.eventRepository.selectUpcomingEvents();
41-
const openedEventIds = new Set(await this.getOpenedEventIds());
47+
await this.scheduleUpcomingReservationsToOpen(comingEvents);
48+
await this.scheduleUpcomingReservationsToClose(comingEvents);
49+
}
50+
51+
private async scheduleUpcomingReservationsToOpen(comingEvents: Event[]) {
4252
const now = new Date();
53+
const openedEventIds = new Set(await this.getOpenedEventIds());
4354
const eventToOpen = comingEvents.filter((event) => event.reservationOpenDate <= now);
44-
const eventsToSchedule = comingEvents.filter(
55+
const eventsToScheduleOpen = comingEvents.filter(
4556
(event) => !openedEventIds.has(event.id) && event.reservationOpenDate > now,
4657
);
4758

4859
for (const event of eventToOpen) {
4960
await this.openReservation(event);
5061
}
51-
for (const event of eventsToSchedule) {
62+
for (const event of eventsToScheduleOpen) {
5263
this.scheduleReservationOpen(event);
5364
}
5465
}
5566

67+
private async scheduleUpcomingReservationsToClose(comingEvents: Event[]) {
68+
const now = new Date();
69+
const eventsToScheduleClose = comingEvents.filter((event) => event.reservationCloseDate > now);
70+
71+
for (const event of eventsToScheduleClose) {
72+
this.scheduleReservationClose(event);
73+
}
74+
}
75+
5676
private scheduleReservationOpen(event: Event) {
5777
const jobName = `reservation-open-${event.id}`;
5878

@@ -68,6 +88,21 @@ export class OpenBookingService implements OnApplicationBootstrap {
6888
job.start();
6989
}
7090

91+
private scheduleReservationClose(event: Event) {
92+
const jobName = `reservation-close-${event.id}`;
93+
94+
if (this.schedulerRegistry.doesExist('cron', jobName)) {
95+
this.schedulerRegistry.deleteCronJob(jobName);
96+
}
97+
98+
const job = new CronJob(event.reservationCloseDate, async () => {
99+
await this.closeReservation(event.id);
100+
});
101+
102+
this.schedulerRegistry.addCronJob(jobName, job);
103+
job.start();
104+
}
105+
71106
async isEventOpened(eventId: number) {
72107
return (await this.redis.get(`open-booking:${eventId}:opened`)) === 'true';
73108
}
@@ -107,22 +142,70 @@ export class OpenBookingService implements OnApplicationBootstrap {
107142
if (!event) {
108143
return false;
109144
}
110-
111145
const openTime = event.reservationOpenDate;
112146
if (openTime > new Date()) {
113-
const jobName = `reservation-open-${event.id}`;
114-
const job = new CronJob(openTime, async () => {
115-
await this.openReservationById(event.id);
116-
});
117-
this.schedulerRegistry.addCronJob(jobName, job);
118-
job.start();
147+
this.scheduleReservationOpen(event);
119148
return false;
120149
}
121-
122150
return true;
123151
}
124152

125153
private async registerOpenedEvent(eventId: number) {
126154
await this.redis.set(`open-booking:${eventId}:opened`, 'true');
127155
}
156+
157+
async closeReservation(eventId: number) {
158+
await this.validateClosingEvent(eventId);
159+
await this.unlinkOpenedEvent(eventId);
160+
await this.clearWaitingService(eventId);
161+
await this.clearEnteringService(eventId);
162+
await this.seatsUpdateService.clearSeatsSubscription(eventId);
163+
await this.clearInBookingService(eventId);
164+
}
165+
166+
private async validateClosingEvent(eventId: number) {
167+
const event = await this.eventRepository.selectEvent(eventId);
168+
if (!event) {
169+
return false;
170+
}
171+
const closeTime = event.reservationCloseDate;
172+
if (closeTime > new Date()) {
173+
this.scheduleReservationClose(event);
174+
return false;
175+
}
176+
return true;
177+
}
178+
179+
private async clearWaitingService(eventId: number) {
180+
const waitingSids = await this.waitingQueueService.getAllWaitingSids(eventId);
181+
for (const sid of waitingSids) {
182+
await this.resetUserStatus(sid);
183+
}
184+
await this.waitingQueueService.clearQueue(eventId);
185+
}
186+
187+
private async clearEnteringService(eventId: number) {
188+
const enteringSids = await this.enterBookingService.getAllEnteringSids(eventId);
189+
for (const sid of enteringSids) {
190+
await this.resetUserStatus(sid);
191+
}
192+
await this.enterBookingService.clearEnteringPool(eventId);
193+
}
194+
195+
private async clearInBookingService(eventId: number) {
196+
const inBookingSids = await this.inBookingService.getAllInBookingSids(eventId);
197+
for (const sid of inBookingSids) {
198+
await this.resetUserStatus(sid);
199+
}
200+
await this.inBookingService.clearInBookingPool(eventId);
201+
}
202+
203+
private async resetUserStatus(sid: string) {
204+
await this.authService.setUserStatusLogin(sid);
205+
await this.userService.setUserEventTarget(sid, 0);
206+
}
207+
208+
private async unlinkOpenedEvent(eventId: number) {
209+
await this.redis.unlink(`open-booking:${eventId}:opened`);
210+
}
128211
}

back/src/domains/booking/service/waiting-queue.service.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,29 @@ export class WaitingQueueService {
9696
}
9797
return headOrder;
9898
}
99+
100+
async getAllWaitingSids(eventId: number) {
101+
return (await this.redis.lrange(`waiting-queue:${eventId}`, 0, -1))
102+
.map((item) => {
103+
try {
104+
const parsed = JSON.parse(item);
105+
return parsed?.sid;
106+
} catch (e) {
107+
return e ? null : null;
108+
}
109+
})
110+
.filter((sid) => sid != null);
111+
}
112+
113+
async clearQueue(eventId: number) {
114+
const subscription = this.queueSubscriptionMap.get(eventId);
115+
if (subscription) {
116+
subscription.complete();
117+
this.queueSubscriptionMap.delete(eventId);
118+
}
119+
const keys = await this.redis.keys(`waiting-queue:${eventId}:*`);
120+
if (keys.length > 0) {
121+
await this.redis.unlink(...keys);
122+
}
123+
}
99124
}

0 commit comments

Comments
 (0)