Skip to content

Commit f812a06

Browse files
committed
Merge branch 'dev' of https://github.com/boostcampwm-2024/web04-RealTicket into feat/#226-layout-api
2 parents 6eec35b + bb63a10 commit f812a06

File tree

12 files changed

+260
-70
lines changed

12 files changed

+260
-70
lines changed
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
export const USER_STATUS = {
22
LOGIN: 'LOGIN',
33
WAITING: 'WAITING',
4+
ENTERING: 'ENTERING',
45
SELECTING_SEAT: 'SELECTING_SEAT',
56
ADMIN: 'ADMIN',
67
};
78

89
export const USER_LEVEL = {
910
LOGIN: 0,
1011
WAITING: 1,
11-
SELECTING_SEAT: 2,
12-
ADMIN: 3,
12+
ENTERING: 2,
13+
SELECTING_SEAT: 3,
14+
ADMIN: 4,
1315
};

back/src/auth/guard/session.guard.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { USER_LEVEL, USER_STATUS } from '../const/userStatus.const';
77

88
const EXPIRE_TIME = 3600;
99

10-
export function SessionAuthGuard(userStatus: string = USER_STATUS.LOGIN) {
10+
export function SessionAuthGuard(requiredStatuses: string | string[] = USER_STATUS.LOGIN) {
1111
@Injectable()
1212
class SessionGuard {
1313
readonly redis: Redis;
@@ -18,21 +18,27 @@ export function SessionAuthGuard(userStatus: string = USER_STATUS.LOGIN) {
1818

1919
async canActivate(context: ExecutionContext): Promise<boolean> {
2020
const request: Request = context.switchToHttp().getRequest();
21-
2221
const sessionId = request.cookies.SID;
23-
const session = JSON.parse(await this.redis.get(`user:${sessionId}`));
24-
// TODO
25-
// userStatus, target_event를 비교하여 접근 허용 여부를 판단
26-
if (session && USER_LEVEL[session.userStatus] >= USER_LEVEL[userStatus]) {
27-
this.redis.expireat(`user:${sessionId}`, Math.round(Date.now() / 1000) + EXPIRE_TIME);
28-
return true;
29-
} else if (!session) {
22+
23+
const sessionData = await this.redis.get(`user:${sessionId}`);
24+
if (!sessionData) {
3025
throw new ForbiddenException('접근 권한이 없습니다.');
31-
} else if (USER_LEVEL[session.userStatus] < USER_LEVEL[userStatus]) {
32-
throw new UnauthorizedException('해당 페이지에 접근할 수 없습니다.');
33-
} else {
26+
}
27+
28+
const session = JSON.parse(sessionData);
29+
if (!session) {
3430
throw new UnauthorizedException('세션이 만료되었습니다.');
3531
}
32+
33+
const statusesToCheck = Array.isArray(requiredStatuses) ? requiredStatuses : [requiredStatuses];
34+
35+
for (const requiredStatus of statusesToCheck) {
36+
if (USER_LEVEL[session.userStatus] >= USER_LEVEL[requiredStatus]) {
37+
await this.redis.expireat(`user:${sessionId}`, Math.round(Date.now() / 1000) + EXPIRE_TIME);
38+
return true;
39+
}
40+
}
41+
throw new UnauthorizedException('해당 페이지에 접근할 수 없습니다.');
3642
}
3743
}
3844

back/src/auth/service/auth.service.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ export class AuthService {
3535
this.redis.set(`user:${sid}`, JSON.stringify({ ...session, userStatus: USER_STATUS.WAITING }));
3636
}
3737

38+
async setUserStatusEntering(sid: string) {
39+
const session = JSON.parse(await this.redis.get(`user:${sid}`));
40+
if (session.userStatus === USER_STATUS.ADMIN) return;
41+
42+
this.redis.set(`user:${sid}`, JSON.stringify({ ...session, userStatus: USER_STATUS.ENTERING }));
43+
}
44+
3845
async setUserStatusSelectingSeat(sid: string) {
3946
const session = JSON.parse(await this.redis.get(`user:${sid}`));
4047
if (session.userStatus === USER_STATUS.ADMIN) return;

back/src/domains/booking/booking.module.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,22 @@ import { UserModule } from '../user/user.module';
99
import { BookingController } from './controller/booking.controller';
1010
import { BookingSeatsService } from './service/booking-seats.service';
1111
import { BookingService } from './service/booking.service';
12+
import { EnterBookingService } from './service/enter-booking.service';
1213
import { InBookingService } from './service/in-booking.service';
1314
import { OpenBookingService } from './service/open-booking.service';
1415
import { WaitingQueueService } from './service/waiting-queue.service';
1516

1617
@Module({
1718
imports: [EventEmitterModule.forRoot(), EventModule, AuthModule, PlaceModule, UserModule],
1819
controllers: [BookingController],
19-
providers: [BookingService, InBookingService, OpenBookingService, BookingSeatsService, WaitingQueueService],
20+
providers: [
21+
BookingService,
22+
InBookingService,
23+
OpenBookingService,
24+
BookingSeatsService,
25+
WaitingQueueService,
26+
EnterBookingService,
27+
],
2028
exports: [InBookingService],
2129
})
2230
export class BookingModule {}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const ENTERING_SESSION_EXPIRY = 30 * 1000;
2+
export const ENTERING_GC_INTERVAL = 10 * 1000;

back/src/domains/booking/controller/booking.controller.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { Request } from 'express';
2626

2727
import { USER_STATUS } from '../../../auth/const/userStatus.const';
2828
import { SessionAuthGuard } from '../../../auth/guard/session.guard';
29+
import { AuthService } from '../../../auth/service/auth.service';
2930
import { SeatStatus } from '../const/seatStatus.enum';
3031
import { BookingAmountReqDto } from '../dto/bookingAmountReq.dto';
3132
import { BookingAmountResDto } from '../dto/bookingAmountRes.dto';
@@ -45,6 +46,7 @@ import { WaitingQueueService } from '../service/waiting-queue.service';
4546
export class BookingController {
4647
constructor(
4748
private readonly eventEmitter: EventEmitter2,
49+
private readonly authService: AuthService,
4850
private readonly bookingService: BookingService,
4951
private readonly inBookingService: InBookingService,
5052
private readonly bookingSeatsService: BookingSeatsService,
@@ -73,8 +75,8 @@ export class BookingController {
7375
return this.waitingQueueService.subscribeQueue(eventId);
7476
}
7577

76-
@UseGuards(SessionAuthGuard(USER_STATUS.SELECTING_SEAT))
7778
@Post('count')
79+
@UseGuards(SessionAuthGuard([USER_STATUS.ENTERING, USER_STATUS.SELECTING_SEAT]))
7880
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
7981
@ApiOperation({ summary: '예매 인원 설정', description: '예매할 인원 수를 설정한다.' })
8082
@ApiBody({ type: BookingAmountReqDto })
@@ -83,22 +85,24 @@ export class BookingController {
8385
@ApiBadRequestResponse({ description: '잘못된 요청' })
8486
async setBookingAmount(@Req() req: Request, @Body() dto: BookingAmountReqDto) {
8587
const sid = req.cookies['SID'];
86-
const result = await this.inBookingService.setBookingAmount(sid, dto.bookingAmount);
88+
const result = await this.bookingService.setBookingAmount(sid, dto.bookingAmount);
8789
return new BookingAmountResDto(result);
8890
}
8991

9092
@Sse('seat/:eventId')
91-
@UseGuards(SessionAuthGuard(USER_STATUS.SELECTING_SEAT))
93+
@UseGuards(SessionAuthGuard([USER_STATUS.ENTERING, USER_STATUS.SELECTING_SEAT]))
9294
@ApiOperation({
9395
summary: '실시간 좌석 예약 현황 SSE',
9496
description: '실시간으로 좌석 예약 현황을 조회한다.',
9597
})
9698
@ApiOkResponse({ description: 'SSE 연결 성공', type: SeatsSseDto })
9799
@ApiUnauthorizedResponse({ description: '인증 실패' })
98100
async getReservationStatusByEventId(@Param('eventId') eventId: number, @Req() req: Request) {
101+
const sid = req.cookies['SID'];
102+
await this.bookingService.setInBookingFromEntering(sid);
103+
99104
const observable = this.bookingSeatsService.subscribeSeats(eventId);
100105

101-
const sid = req.cookies['SID'];
102106
req.on('close', () => {
103107
this.eventEmitter.emit('seats-sse-close', { sid });
104108
});

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

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1+
import { RedisService } from '@liaoliaots/nestjs-redis';
12
import { BadRequestException, Injectable, InternalServerErrorException, Logger } from '@nestjs/common';
23
import { OnEvent } from '@nestjs/event-emitter';
4+
import Redis from 'ioredis';
35

46
import { AuthService } from '../../../auth/service/auth.service';
57
import { EventService } from '../../event/service/event.service';
68
import { UserService } from '../../user/service/user.service';
79
import { BookingAdmissionStatusDto } from '../dto/bookingAdmissionStatus.dto';
810
import { ServerTimeDto } from '../dto/serverTime.dto';
911

12+
import { BookingSeatsService } from './booking-seats.service';
13+
import { EnterBookingService } from './enter-booking.service';
1014
import { InBookingService } from './in-booking.service';
1115
import { OpenBookingService } from './open-booking.service';
1216
import { WaitingQueueService } from './waiting-queue.service';
@@ -16,30 +20,84 @@ const OFFSET = 1000 * 60 * 60 * 9;
1620
@Injectable()
1721
export class BookingService {
1822
private logger = new Logger(BookingService.name);
23+
private readonly redis: Redis | null;
1924
constructor(
25+
private readonly redisService: RedisService,
2026
private readonly eventService: EventService,
2127
private readonly authService: AuthService,
28+
private readonly bookingSeatsService: BookingSeatsService,
2229
private readonly inBookingService: InBookingService,
2330
private readonly openBookingService: OpenBookingService,
2431
private readonly waitingQueueService: WaitingQueueService,
2532
private readonly userService: UserService,
26-
) {}
33+
private readonly enterBookingService: EnterBookingService,
34+
) {
35+
this.redis = this.redisService.getOrThrow();
36+
}
2737

2838
@OnEvent('seats-sse-close')
29-
async letInNextWaiting(event: { sid: string }) {
30-
const eventId = await this.userService.getUserEventTarget(event.sid);
31-
if ((await this.waitingQueueService.getQueueSize(eventId)) < 1) {
32-
return;
39+
async onSeatsSseDisconnected(event: { sid: string }) {
40+
const sid = event.sid;
41+
const eventId = await this.userService.getUserEventTarget(sid);
42+
await this.collectSeatsIfNotSaved(eventId, sid);
43+
await this.inBookingService.emitSession(sid);
44+
await this.letInNextWaiting(eventId);
45+
}
46+
47+
private async collectSeatsIfNotSaved(eventId: number, sid: string) {
48+
const inBookingSession = await this.inBookingService.getSession(eventId, sid);
49+
if (inBookingSession && !inBookingSession.saved) {
50+
const bookedSeats = inBookingSession.bookedSeats;
51+
bookedSeats.forEach((seat) => {
52+
this.bookingSeatsService.updateSeatDeleted(eventId, seat);
53+
});
54+
inBookingSession.bookedSeats = [];
55+
await this.inBookingService.setSession(eventId, inBookingSession);
3356
}
34-
if (await this.inBookingService.isInsertable(eventId)) {
57+
}
58+
59+
@OnEvent('entering-sessions-gc')
60+
async onEnteringSessionsGc(event: { eventId: number }) {
61+
await this.letInNextWaiting(event.eventId);
62+
}
63+
64+
@OnEvent('in-booking-max-size-changed')
65+
async onSpecificInBookingMaxSizeChanged(event: { eventId: number }) {
66+
await this.letInNextWaiting(event.eventId);
67+
}
68+
69+
@OnEvent('all-in-booking-max-size-changed')
70+
async onAllInBookingMaxSizeChanged() {
71+
const eventIds = await this.openBookingService.getOpenedEventIds();
72+
await Promise.all(
73+
eventIds.map(async (eventId) => {
74+
await this.letInNextWaiting(eventId);
75+
}),
76+
);
77+
}
78+
79+
private async letInNextWaiting(eventId: number) {
80+
const isQueueEmpty = async (eventId: number) =>
81+
(await this.waitingQueueService.getQueueSize(eventId)) < 1;
82+
while (!(await isQueueEmpty(eventId)) && (await this.isInsertableInBooking(eventId))) {
3583
const item = await this.waitingQueueService.popQueue(eventId);
3684
if (!item) {
37-
return;
85+
break;
3886
}
39-
await this.authService.setUserStatusSelectingSeat(item.sid);
87+
await this.enterBookingService.addEnteringSession(item.sid);
88+
await this.authService.setUserStatusEntering(item.sid);
4089
}
4190
}
4291

92+
async setInBookingFromEntering(sid: string) {
93+
const eventId = await this.userService.getUserEventTarget(sid);
94+
const bookingAmount = await this.enterBookingService.getBookingAmount(sid);
95+
96+
await this.enterBookingService.removeEnteringSession(sid);
97+
await this.inBookingService.insertInBooking(eventId, sid, bookingAmount);
98+
await this.authService.setUserStatusSelectingSeat(sid);
99+
}
100+
43101
// 함수 이름 생각하기
44102
async isAdmission(eventId: number, sid: string): Promise<BookingAdmissionStatusDto> {
45103
// eventId를 받아서 해당 이벤트가 존재하는지 확인한다.
@@ -61,10 +119,12 @@ export class BookingService {
61119
}
62120

63121
private async getForwarded(sid: string) {
64-
const isEntered = await this.inBookingService.insertIfPossible(sid);
122+
const eventId = await this.userService.getUserEventTarget(sid);
123+
const isInsertable = await this.isInsertableInBooking(eventId);
65124

66-
if (isEntered) {
67-
await this.authService.setUserStatusSelectingSeat(sid);
125+
if (isInsertable) {
126+
await this.enterBookingService.addEnteringSession(sid);
127+
await this.authService.setUserStatusEntering(sid);
68128
return {
69129
waitingStatus: false,
70130
enteringStatus: true,
@@ -80,6 +140,21 @@ export class BookingService {
80140
};
81141
}
82142

143+
private async isInsertableInBooking(eventId: number): Promise<boolean> {
144+
const inBookingCount = await this.inBookingService.getInBookingSessionCount(eventId);
145+
const enteringCount = await this.enterBookingService.getEnteringSessionCount(eventId);
146+
const maxSize = await this.inBookingService.getInBookingSessionsMaxSize(eventId);
147+
return inBookingCount + enteringCount < maxSize;
148+
}
149+
150+
async setBookingAmount(sid: string, bookingAmount: number) {
151+
const isInBooking = await this.inBookingService.isInBooking(sid);
152+
if (isInBooking) {
153+
return await this.inBookingService.setBookingAmount(sid, bookingAmount);
154+
}
155+
return await this.enterBookingService.setBookingAmount(sid, bookingAmount);
156+
}
157+
83158
async getTimeMs(): Promise<ServerTimeDto> {
84159
try {
85160
return {
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { RedisService } from '@liaoliaots/nestjs-redis';
2+
import { Injectable } from '@nestjs/common';
3+
import { EventEmitter2 } from '@nestjs/event-emitter';
4+
import Redis from 'ioredis';
5+
6+
import { UserService } from '../../user/service/user.service';
7+
import { ENTERING_GC_INTERVAL, ENTERING_SESSION_EXPIRY } from '../const/enterBooking.const';
8+
9+
@Injectable()
10+
export class EnterBookingService {
11+
private readonly redis: Redis | null;
12+
constructor(
13+
private readonly redisService: RedisService,
14+
private readonly userService: UserService,
15+
private readonly eventEmitter: EventEmitter2,
16+
) {
17+
this.redis = this.redisService.getOrThrow();
18+
}
19+
20+
async gcEnteringSessions(eventId: number) {
21+
setInterval(() => {
22+
this.removeExpiredSessions(eventId);
23+
this.eventEmitter.emit('entering-sessions-gc', { eventId });
24+
}, ENTERING_GC_INTERVAL);
25+
}
26+
27+
async addEnteringSession(sid: string) {
28+
const eventId = await this.userService.getUserEventTarget(sid);
29+
const timestamp = Date.now();
30+
await this.redis.zadd(`entering:${eventId}`, timestamp, sid);
31+
return true;
32+
}
33+
34+
async removeEnteringSession(sid: string) {
35+
const eventId = await this.userService.getUserEventTarget(sid);
36+
await this.redis.zrem(`entering:${eventId}`, sid);
37+
await this.removeBookingAmount(sid);
38+
return true;
39+
}
40+
41+
async getEnteringSessionCount(eventId: number) {
42+
return this.redis.zcard(`entering:${eventId}`);
43+
}
44+
45+
async setBookingAmount(sid: string, bookingAmount: number) {
46+
await this.redis.set(`entering:${sid}:temp-booking-amount`, bookingAmount);
47+
return parseInt(await this.redis.get(`entering:${sid}:temp-booking-amount`));
48+
}
49+
50+
async getBookingAmount(sid: string) {
51+
const bookingAmountData = await this.redis.get(`entering:${sid}:temp-booking-amount`);
52+
if (!bookingAmountData) {
53+
return 0;
54+
}
55+
return parseInt(await this.redis.get(`entering:${sid}:temp-booking-amount`));
56+
}
57+
58+
async removeBookingAmount(sid: string) {
59+
await this.redis.del(`entering:${sid}:temp-booking-amount`);
60+
return true;
61+
}
62+
63+
private async removeExpiredSessions(eventId: number) {
64+
const expiryTimestamp = Date.now() - ENTERING_SESSION_EXPIRY;
65+
await this.redis.zremrangebyscore(`entering:${eventId}`, 0, expiryTimestamp);
66+
}
67+
}

0 commit comments

Comments
 (0)