Skip to content

Commit

Permalink
refactor: [BE] redis event 구독 부분 서비스로 분리
Browse files Browse the repository at this point in the history
  • Loading branch information
DongHoonYu96 committed Nov 19, 2024
1 parent ec1c320 commit 5ec9e03
Show file tree
Hide file tree
Showing 9 changed files with 446 additions and 227 deletions.
36 changes: 36 additions & 0 deletions BE/src/common/redis/redis-subscriber.service.ts
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}`);
}
}
}
58 changes: 58 additions & 0 deletions BE/src/common/redis/subscribers/base.subscriber.ts
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
};
}
}
80 changes: 80 additions & 0 deletions BE/src/common/redis/subscribers/player.subscriber.ts
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}`);
}
}
63 changes: 63 additions & 0 deletions BE/src/common/redis/subscribers/room.subscriber.ts
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;
}
}
}
55 changes: 55 additions & 0 deletions BE/src/common/redis/subscribers/scoring.subscriber.ts
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');
}
}
Loading

0 comments on commit 5ec9e03

Please sign in to comment.