-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: [BE] redis event 구독 부분 서비스로 분리
- Loading branch information
1 parent
ec1c320
commit 5ec9e03
Showing
9 changed files
with
446 additions
and
227 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { Injectable, Logger } from '@nestjs/common'; | ||
import { InjectRedis } from '@nestjs-modules/ioredis'; | ||
import Redis from 'ioredis'; | ||
import { ScoringSubscriber } from './subscribers/scoring.subscriber'; | ||
import { RedisSubscriber } from './subscribers/base.subscriber'; | ||
import { TimerSubscriber } from './subscribers/timer.subscriber'; | ||
import { RoomSubscriber } from './subscribers/room.subscriber'; | ||
import { PlayerSubscriber } from './subscribers/player.subscriber'; | ||
import { Server } from 'socket.io'; | ||
|
||
@Injectable() | ||
export class RedisSubscriberService { | ||
private readonly logger = new Logger(RedisSubscriberService.name); | ||
private readonly subscribers: RedisSubscriber[] = []; | ||
|
||
constructor( | ||
@InjectRedis() private readonly redis: Redis, | ||
private readonly scoringSubscriber: ScoringSubscriber, | ||
private readonly timerSubscriber: TimerSubscriber, | ||
private readonly roomSubscriber: RoomSubscriber, | ||
private readonly playerSubscriber: PlayerSubscriber | ||
) { | ||
this.subscribers = [scoringSubscriber, timerSubscriber, roomSubscriber, playerSubscriber]; | ||
} | ||
|
||
async initializeSubscribers(server: Server) { | ||
// Redis Keyspace Notification 설정 | ||
await this.redis.config('SET', 'notify-keyspace-events', 'KEhx'); | ||
|
||
// 각 Subscriber 초기화 | ||
for (const subscriber of this.subscribers) { | ||
await subscriber.subscribe(server); | ||
this.logger.verbose(`Initialized ${subscriber.constructor.name}`); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { InjectRedis } from '@nestjs-modules/ioredis'; | ||
import Redis from 'ioredis'; | ||
import { Logger } from '@nestjs/common'; | ||
import { Server } from 'socket.io'; | ||
import { REDIS_KEY } from '../../constants/redis-key.constant'; | ||
|
||
export abstract class RedisSubscriber { | ||
protected readonly logger: Logger; | ||
|
||
protected constructor(@InjectRedis() protected readonly redis: Redis) { | ||
this.logger = new Logger(this.constructor.name); | ||
} | ||
|
||
abstract subscribe(server: Server): Promise<void>; | ||
|
||
async getQuizResults(gameId: string) { | ||
// 1. 현재 퀴즈 정보 가져오기 | ||
const currentQuiz = await this.redis.get(REDIS_KEY.ROOM_CURRENT_QUIZ(gameId)); | ||
const [quizNum] = currentQuiz.split(':'); | ||
|
||
// 2. 퀴즈 상세 정보 가져오기 | ||
const quizList = await this.redis.smembers(REDIS_KEY.ROOM_QUIZ_SET(gameId)); | ||
const quiz = (await this.redis.hgetall( | ||
REDIS_KEY.ROOM_QUIZ(gameId, quizList[parseInt(quizNum)]) | ||
)) as any; | ||
|
||
// 3. 리더보드 정보 가져오기 | ||
const leaderboard = await this.redis.zrange( | ||
REDIS_KEY.ROOM_LEADERBOARD(gameId), | ||
0, | ||
-1, | ||
'WITHSCORES' | ||
); | ||
|
||
// 4. 플레이어 정보 구성 | ||
const players = []; | ||
for (let i = 0; i < leaderboard.length; i += 2) { | ||
const playerId = leaderboard[i]; | ||
const score = parseInt(leaderboard[i + 1]); | ||
const isAnswer = | ||
(await this.redis.hget(REDIS_KEY.PLAYER(playerId), 'isAnswerCorrect')) === '1'; | ||
|
||
players.push({ | ||
playerId, | ||
score, | ||
isAnswer | ||
}); | ||
} | ||
|
||
return { | ||
quiz: { | ||
...quiz, | ||
quizNum: parseInt(quizNum) | ||
}, | ||
players | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import { Injectable } from '@nestjs/common'; | ||
import { RedisSubscriber } from './base.subscriber'; | ||
import { InjectRedis } from '@nestjs-modules/ioredis'; | ||
import Redis from 'ioredis'; | ||
import { Server } from 'socket.io'; | ||
import SocketEvents from '../../constants/socket-events'; | ||
|
||
@Injectable() | ||
export class PlayerSubscriber extends RedisSubscriber { | ||
constructor(@InjectRedis() redis: Redis) { | ||
super(redis); | ||
} | ||
|
||
async subscribe(server: Server): Promise<void> { | ||
const subscriber = this.redis.duplicate(); | ||
await subscriber.psubscribe('__keyspace@0__:Player:*'); | ||
|
||
subscriber.on('pmessage', async (_pattern, channel, message) => { | ||
const playerId = this.extractPlayerId(channel); | ||
if (!playerId || message !== 'hset') { | ||
return; | ||
} | ||
|
||
const key = `Player:${playerId}`; | ||
await this.handlePlayerChanges(key, playerId, server); | ||
}); | ||
} | ||
|
||
private extractPlayerId(channel: string): string | null { | ||
const splitKey = channel.replace('__keyspace@0__:', '').split(':'); | ||
return splitKey.length === 2 ? splitKey[1] : null; | ||
} | ||
|
||
private async handlePlayerChanges(key: string, playerId: string, server: Server) { | ||
const changes = await this.redis.get(`${key}:Changes`); | ||
const playerData = await this.redis.hgetall(key); | ||
|
||
switch (changes) { | ||
case 'Join': | ||
await this.handlePlayerJoin(playerId, playerData, server); | ||
break; | ||
|
||
case 'Position': | ||
await this.handlePlayerPosition(playerId, playerData, server); | ||
break; | ||
|
||
case 'Disconnect': | ||
await this.handlePlayerDisconnect(playerId, playerData, server); | ||
break; | ||
} | ||
} | ||
|
||
private async handlePlayerJoin(playerId: string, playerData: any, server: Server) { | ||
const newPlayer = { | ||
playerId, | ||
playerName: playerData.playerName, | ||
playerPosition: [parseFloat(playerData.positionX), parseFloat(playerData.positionY)] | ||
}; | ||
|
||
server.to(playerData.gameId).emit(SocketEvents.JOIN_ROOM, { | ||
players: [newPlayer] | ||
}); | ||
this.logger.verbose(`Player joined: ${playerId} to game: ${playerData.gameId}`); | ||
} | ||
|
||
private async handlePlayerPosition(playerId: string, playerData: any, server: Server) { | ||
server.to(playerData.gameId).emit(SocketEvents.UPDATE_POSITION, { | ||
playerId, | ||
playerPosition: [parseFloat(playerData.positionX), parseFloat(playerData.positionY)] | ||
}); | ||
this.logger.verbose(`Player position updated: ${playerId}`); | ||
} | ||
|
||
private async handlePlayerDisconnect(playerId: string, playerData: any, server: Server) { | ||
server.to(playerData.gameId).emit(SocketEvents.EXIT_ROOM, { | ||
playerId | ||
}); | ||
this.logger.verbose(`Player disconnected: ${playerId} from game: ${playerData.gameId}`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { Injectable } from '@nestjs/common'; | ||
import { RedisSubscriber } from './base.subscriber'; | ||
import { InjectRedis } from '@nestjs-modules/ioredis'; | ||
import Redis from 'ioredis'; | ||
import { Server } from 'socket.io'; | ||
import SocketEvents from '../../constants/socket-events'; | ||
|
||
@Injectable() | ||
export class RoomSubscriber extends RedisSubscriber { | ||
constructor(@InjectRedis() redis: Redis) { | ||
super(redis); | ||
} | ||
|
||
async subscribe(server: Server): Promise<void> { | ||
const subscriber = this.redis.duplicate(); | ||
await subscriber.psubscribe('__keyspace@0__:Room:*'); | ||
|
||
subscriber.on('pmessage', async (_pattern, channel, message) => { | ||
const gameId = this.extractGameId(channel); | ||
if (!gameId || message !== 'hset') { | ||
return; | ||
} | ||
|
||
const key = `Room:${gameId}`; | ||
await this.handleRoomChanges(key, gameId, server); | ||
}); | ||
} | ||
|
||
private extractGameId(channel: string): string | null { | ||
const splitKey = channel.replace('__keyspace@0__:', '').split(':'); | ||
return splitKey.length === 2 ? splitKey[1] : null; | ||
} | ||
|
||
private async handleRoomChanges(key: string, gameId: string, server: Server) { | ||
const changes = await this.redis.get(`${key}:Changes`); | ||
const roomData = await this.redis.hgetall(key); | ||
|
||
switch (changes) { | ||
case 'Option': | ||
server.to(gameId).emit(SocketEvents.UPDATE_ROOM_OPTION, { | ||
title: roomData.title, | ||
gameMode: roomData.gameMode, | ||
maxPlayerCount: roomData.maxPlayerCount, | ||
isPublic: roomData.isPublic | ||
}); | ||
this.logger.verbose(`Room option updated: ${gameId}`); | ||
break; | ||
|
||
case 'Quizset': | ||
server.to(gameId).emit(SocketEvents.UPDATE_ROOM_QUIZSET, { | ||
quizSetId: roomData.quizSetId, | ||
quizCount: roomData.quizCount | ||
}); | ||
this.logger.verbose(`Room quizset updated: ${gameId}`); | ||
break; | ||
|
||
case 'Start': | ||
server.to(gameId).emit(SocketEvents.START_GAME, ''); | ||
this.logger.verbose(`Game started: ${gameId}`); | ||
break; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { Injectable } from '@nestjs/common'; | ||
import { RedisSubscriber } from './base.subscriber'; | ||
import { InjectRedis } from '@nestjs-modules/ioredis'; | ||
import Redis from 'ioredis'; | ||
import { Server } from 'socket.io'; | ||
import { REDIS_KEY } from '../../constants/redis-key.constant'; | ||
import SocketEvents from '../../constants/socket-events'; | ||
|
||
@Injectable() | ||
export class ScoringSubscriber extends RedisSubscriber { | ||
private scoringMap = new Map<string, number>(); | ||
|
||
constructor(@InjectRedis() redis: Redis) { | ||
super(redis); | ||
} | ||
|
||
async subscribe(server: Server): Promise<void> { | ||
const subscriber = this.redis.duplicate(); | ||
await subscriber.psubscribe('scoring:*'); | ||
|
||
subscriber.on('pmessage', async (_pattern, channel, message) => { | ||
const gameId = channel.split(':')[1]; | ||
await this.handleScoring(gameId, parseInt(message), server); | ||
}); | ||
} | ||
|
||
private async handleScoring(gameId: string, completeClientsCount: number, server: Server) { | ||
if (!this.scoringMap.has(gameId)) { | ||
this.scoringMap[gameId] = 0; | ||
} | ||
this.scoringMap[gameId] += completeClientsCount; | ||
|
||
const playersCount = await this.redis.scard(REDIS_KEY.ROOM_PLAYERS(gameId)); | ||
if (this.scoringMap[gameId] >= playersCount) { | ||
await this.completeScoring(gameId, server); | ||
} | ||
} | ||
|
||
private async completeScoring(gameId: string, server: Server) { | ||
const { quiz, players } = await this.getQuizResults(gameId); | ||
|
||
server.to(gameId).emit(SocketEvents.END_QUIZ_TIME, { | ||
answer: quiz.answer, | ||
players | ||
}); | ||
|
||
await this.updateQuizState(gameId, quiz.quizNum); | ||
this.logger.verbose(`endQuizTime: ${gameId} - ${quiz.quizNum}`); | ||
} | ||
|
||
private async updateQuizState(gameId: string, quizNum: number) { | ||
await this.redis.set(REDIS_KEY.ROOM_CURRENT_QUIZ(gameId), `${quizNum}:end`); | ||
await this.redis.set(REDIS_KEY.ROOM_TIMER(gameId), 'timer', 'EX', '10', 'NX'); | ||
} | ||
} |
Oops, something went wrong.