diff --git a/BE/src/common/redis/redis-subscriber.service.ts b/BE/src/common/redis/redis-subscriber.service.ts new file mode 100644 index 0000000..c1e4f6c --- /dev/null +++ b/BE/src/common/redis/redis-subscriber.service.ts @@ -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}`); + } + } +} diff --git a/BE/src/common/redis/subscribers/base.subscriber.ts b/BE/src/common/redis/subscribers/base.subscriber.ts new file mode 100644 index 0000000..0d7a00b --- /dev/null +++ b/BE/src/common/redis/subscribers/base.subscriber.ts @@ -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; + + 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 + }; + } +} diff --git a/BE/src/common/redis/subscribers/player.subscriber.ts b/BE/src/common/redis/subscribers/player.subscriber.ts new file mode 100644 index 0000000..ab47b03 --- /dev/null +++ b/BE/src/common/redis/subscribers/player.subscriber.ts @@ -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 { + 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}`); + } +} diff --git a/BE/src/common/redis/subscribers/room.subscriber.ts b/BE/src/common/redis/subscribers/room.subscriber.ts new file mode 100644 index 0000000..bbf2164 --- /dev/null +++ b/BE/src/common/redis/subscribers/room.subscriber.ts @@ -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 { + 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; + } + } +} diff --git a/BE/src/common/redis/subscribers/scoring.subscriber.ts b/BE/src/common/redis/subscribers/scoring.subscriber.ts new file mode 100644 index 0000000..f142e47 --- /dev/null +++ b/BE/src/common/redis/subscribers/scoring.subscriber.ts @@ -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(); + + constructor(@InjectRedis() redis: Redis) { + super(redis); + } + + async subscribe(server: Server): Promise { + 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'); + } +} diff --git a/BE/src/common/redis/subscribers/timer.subscriber.ts b/BE/src/common/redis/subscribers/timer.subscriber.ts new file mode 100644 index 0000000..b2d9fde --- /dev/null +++ b/BE/src/common/redis/subscribers/timer.subscriber.ts @@ -0,0 +1,129 @@ +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 TimerSubscriber extends RedisSubscriber { + constructor( + @InjectRedis() redis: Redis // 부모에게 전달 + ) { + super(redis); + } + + async subscribe(server: Server): Promise { + const subscriber = this.redis.duplicate(); + await subscriber.psubscribe(`__keyspace@0__:${REDIS_KEY.ROOM_TIMER('*')}`); + + subscriber.on('pmessage', async (_pattern, channel, message) => { + const gameId = this.extractGameId(channel); + if (!gameId || message !== 'expired') { + return; + } + + const currentQuiz = await this.redis.get(REDIS_KEY.ROOM_CURRENT_QUIZ(gameId)); + const [quizNum, state] = currentQuiz.split(':'); + + if (state === 'start') { + await this.handleQuizScoring(gameId, parseInt(quizNum), server); + } else { + await this.handleNextQuiz(gameId, parseInt(quizNum), server); + } + }); + } + + private extractGameId(channel: string): string | null { + const splitKey = channel.replace('__keyspace@0__:', '').split(':'); + return splitKey.length === 3 ? splitKey[1] : null; + } + + private async handleQuizScoring(gameId: string, quizNum: number, server: Server) { + const quizList = await this.redis.smembers(REDIS_KEY.ROOM_QUIZ_SET(gameId)); + const quiz = await this.redis.hgetall(REDIS_KEY.ROOM_QUIZ(gameId, quizList[quizNum])); + + const sockets = await server.in(gameId).fetchSockets(); + const clients = sockets.map((socket) => socket.id); + const correctPlayers = []; + + // 플레이어 답안 처리 + for (const clientId of clients) { + const player = await this.redis.hgetall(REDIS_KEY.PLAYER(clientId)); + const selectAnswer = this.calculateAnswer(player.positionX, player.positionY); + + await this.redis.set(`${REDIS_KEY.PLAYER(clientId)}:Changes`, 'AnswerCorrect'); + if (selectAnswer.toString() === quiz.answer) { + correctPlayers.push(clientId); + await this.redis.hmset(REDIS_KEY.PLAYER(clientId), { isAnswerCorrect: '1' }); + } else { + await this.redis.hmset(REDIS_KEY.PLAYER(clientId), { isAnswerCorrect: '0' }); + } + } + + // 점수 업데이트 + for (const clientId of correctPlayers) { + await this.redis.zincrby( + REDIS_KEY.ROOM_LEADERBOARD(gameId), + 1000 / correctPlayers.length, + clientId + ); + } + + await this.redis.publish(`scoring:${gameId}`, clients.length.toString()); + this.logger.verbose(`채점: ${gameId} - ${clients.length}`); + } + + private async handleNextQuiz(gameId: string, currentQuizNum: number, server: Server) { + const newQuizNum = currentQuizNum + 1; + const quizList = await this.redis.smembers(REDIS_KEY.ROOM_QUIZ_SET(gameId)); + + if (quizList.length <= newQuizNum) { + const leaderboard = await this.redis.zrange( + REDIS_KEY.ROOM_LEADERBOARD(gameId), + 0, + -1, + 'WITHSCORES' + ); + + server.to(gameId).emit(SocketEvents.END_GAME, { + host: leaderboard[0] + }); + this.logger.verbose(`endGame: ${leaderboard[0]}`); + return; + } + + const quiz = await this.redis.hgetall(REDIS_KEY.ROOM_QUIZ(gameId, quizList[newQuizNum])); + const quizChoices = await this.redis.hgetall( + REDIS_KEY.ROOM_QUIZ_CHOICES(gameId, quizList[newQuizNum]) + ); + + server.to(gameId).emit(SocketEvents.START_QUIZ_TIME, { + quiz: quiz.quiz, + choiceList: Object.entries(quizChoices).map(([key, value]) => ({ + order: key, + content: value + })), + startTime: Date.now() + 3000, + endTime: Date.now() + (parseInt(quiz.limitTime) + 3) * 1000 + }); + + await this.redis.set(REDIS_KEY.ROOM_CURRENT_QUIZ(gameId), `${newQuizNum}:start`); + await this.redis.set( + REDIS_KEY.ROOM_TIMER(gameId), + 'timer', + 'EX', + (parseInt(quiz.limitTime) + 3).toString(), + 'NX' + ); + this.logger.verbose(`startQuizTime: ${gameId} - ${newQuizNum}`); + } + + private calculateAnswer(positionX: string, positionY: string): number { + if (parseFloat(positionY) < 0.5) { + return parseFloat(positionX) < 0.5 ? 1 : 2; + } + return parseFloat(positionX) < 0.5 ? 3 : 4; + } +} diff --git a/BE/src/game/game.module.ts b/BE/src/game/game.module.ts index a254119..87486c1 100644 --- a/BE/src/game/game.module.ts +++ b/BE/src/game/game.module.ts @@ -8,6 +8,11 @@ import { GameRoomService } from './service/game.room.service'; import { QuizCacheService } from './service/quiz.cache.service'; import { QuizSetModule } from '../quiz-set/quiz-set.module'; import { QuizSetService } from '../quiz-set/service/quiz-set.service'; +import { ScoringSubscriber } from '../common/redis/subscribers/scoring.subscriber'; +import { TimerSubscriber } from '../common/redis/subscribers/timer.subscriber'; +import { RoomSubscriber } from '../common/redis/subscribers/room.subscriber'; +import { PlayerSubscriber } from '../common/redis/subscribers/player.subscriber'; +import { RedisSubscriberService } from '../common/redis/redis-subscriber.service'; @Module({ imports: [RedisModule, QuizSetModule], @@ -18,7 +23,12 @@ import { QuizSetService } from '../quiz-set/service/quiz-set.service'; GameRoomService, GameValidator, QuizSetService, - QuizCacheService + QuizCacheService, + RedisSubscriberService, + ScoringSubscriber, + TimerSubscriber, + RoomSubscriber, + PlayerSubscriber ], exports: [GameService] }) diff --git a/BE/src/game/service/game.service.ts b/BE/src/game/service/game.service.ts index 2d1c5ae..394b4cd 100644 --- a/BE/src/game/service/game.service.ts +++ b/BE/src/game/service/game.service.ts @@ -9,6 +9,7 @@ import { StartGameDto } from '../dto/start-game.dto'; import { Server } from 'socket.io'; import { mockQuizData } from '../../../test/mocks/quiz-data.mock'; import { QuizCacheService } from './quiz.cache.service'; +import { RedisSubscriberService } from '../../common/redis/redis-subscriber.service'; @Injectable() export class GameService { @@ -18,7 +19,8 @@ export class GameService { constructor( @InjectRedis() private readonly redis: Redis, private readonly gameValidator: GameValidator, - private readonly quizCacheService: QuizCacheService + private readonly quizCacheService: QuizCacheService, + private readonly redisSubscriberService: RedisSubscriberService ) {} async updatePosition(updatePosition: UpdatePositionDto, clientId: string) { @@ -124,231 +126,7 @@ export class GameService { } async subscribeRedisEvent(server: Server) { - this.redis.config('SET', 'notify-keyspace-events', 'KEhx'); - - // TODO: 분리 필요 - - const scoringSubscriber = this.redis.duplicate(); - await scoringSubscriber.psubscribe('scoring:*'); - scoringSubscriber.on('pmessage', async (_pattern, channel, message) => { - const gameId = channel.split(':')[1]; - - const completeClientsCount = parseInt(message); - 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) { - // 채점 완료! - const currentQuiz = await this.redis.get(REDIS_KEY.ROOM_CURRENT_QUIZ(gameId)); - const splitCurrentQuiz = currentQuiz.split(':'); - - const quizNum = parseInt(splitCurrentQuiz[0]); - const quizList = await this.redis.smembers(REDIS_KEY.ROOM_QUIZ_SET(gameId)); - const quiz = await this.redis.hgetall(REDIS_KEY.ROOM_QUIZ(gameId, quizList[quizNum])); - - const leaderboard = await this.redis.zrange( - REDIS_KEY.ROOM_LEADERBOARD(gameId), - 0, - -1, - 'WITHSCORES' - ); - const players = []; - for (let i = 0; i < leaderboard.length; i += 2) { - players.push({ - playerId: leaderboard[i], - score: parseInt(leaderboard[i + 1]), - isAnswer: - (await this.redis.hget(REDIS_KEY.PLAYER(leaderboard[i]), 'isAnswerCorrect')) === '1' - }); - } - server.to(gameId).emit(SocketEvents.END_QUIZ_TIME, { - answer: quiz.answer, - players: players - }); - - 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'); // 타이머 설정 - this.logger.verbose(`endQuizTime: ${gameId} - ${quizNum}`); - } - }); - - const timerSubscriber = this.redis.duplicate(); - await timerSubscriber.psubscribe(`__keyspace@0__:${REDIS_KEY.ROOM_TIMER('*')}`); - timerSubscriber.on('pmessage', async (_pattern, channel, message) => { - const key = channel.replace('__keyspace@0__:', ''); - const splitKey = key.split(':'); - if (splitKey.length !== 3) { - return; - } - const gameId = splitKey[1]; - - if (message === 'expired') { - const currentQuiz = await this.redis.get(REDIS_KEY.ROOM_CURRENT_QUIZ(gameId)); - const splitCurrentQuiz = currentQuiz.split(':'); - const quizNum = parseInt(splitCurrentQuiz[0]); - if (splitCurrentQuiz[1] === 'start') { - // 채점 - const quizList = await this.redis.smembers(REDIS_KEY.ROOM_QUIZ_SET(gameId)); - const quiz = await this.redis.hgetall(REDIS_KEY.ROOM_QUIZ(gameId, quizList[quizNum])); - // gameId를 통해 해당 room에 있는 client id들을 받기 - const sockets = await server.in(gameId).fetchSockets(); - const clients = sockets.map((socket) => socket.id); - const correctPlayers = []; - for (const clientId of clients) { - const player = await this.redis.hgetall(REDIS_KEY.PLAYER(clientId)); - // 임시로 4개 선택지의 경우만 선정 (1 2 / 3 4) - let selectAnswer = 0; - if (parseFloat(player.positionY) < 0.5) { - if (parseFloat(player.positionX) < 0.5) { - selectAnswer = 1; - } else { - selectAnswer = 2; - } - } else { - if (parseFloat(player.positionX) < 0.5) { - selectAnswer = 3; - } else { - selectAnswer = 4; - } - } - await this.redis.set(`${REDIS_KEY.PLAYER(clientId)}:Changes`, 'AnswerCorrect'); - if (selectAnswer.toString() === quiz.answer) { - correctPlayers.push(clientId); - await this.redis.hmset(REDIS_KEY.PLAYER(clientId), { isAnswerCorrect: '1' }); - } else { - await this.redis.hmset(REDIS_KEY.PLAYER(clientId), { isAnswerCorrect: '0' }); - } - } - for (const clientId of correctPlayers) { - await this.redis.zincrby( - REDIS_KEY.ROOM_LEADERBOARD(gameId), - 1000 / correctPlayers.length, - clientId - ); - } - await this.redis.publish(`scoring:${gameId}`, clients.length.toString()); - this.logger.verbose(`채점: ${gameId} - ${clients.length}`); - } else { - // startQuizTime 하는 부분 - const newQuizNum = quizNum + 1; - const quizList = await this.redis.smembers(REDIS_KEY.ROOM_QUIZ_SET(gameId)); - if (quizList.length <= newQuizNum) { - // 마지막 퀴즈이면, 게임 종료! - // 1등을 새로운 호스트로 설정 - const leaderboard = await this.redis.zrange( - REDIS_KEY.ROOM_LEADERBOARD(gameId), - 0, - -1, - 'WITHSCORES' - ); - // const players = []; - // for (let i = 0; i < leaderboard.length; i += 2) { - // players.push({ - // playerId: leaderboard[i], - // score: parseInt(leaderboard[i + 1]) - // }); - // } - server.to(gameId).emit(SocketEvents.END_GAME, { - host: leaderboard[0] // 아마 첫 번째가 1등..? - }); - this.logger.verbose(`endGame: ${leaderboard[0]}`); - return; - } - const quiz = await this.redis.hgetall(REDIS_KEY.ROOM_QUIZ(gameId, quizList[newQuizNum])); - const quizChoices = await this.redis.hgetall( - REDIS_KEY.ROOM_QUIZ_CHOICES(gameId, quizList[newQuizNum]) - ); - server.to(gameId).emit(SocketEvents.START_QUIZ_TIME, { - quiz: quiz.quiz, - choiceList: Object.entries(quizChoices).map(([key, value]) => ({ - order: key, - content: value - })), - startTime: Date.now() + 3000, - endTime: Date.now() + (parseInt(quiz.limitTime) + 3) * 1000 - }); - await this.redis.set(REDIS_KEY.ROOM_CURRENT_QUIZ(gameId), `${newQuizNum}:start`); // 현재 퀴즈 상태를 시작 상태로 변경 - await this.redis.set( - REDIS_KEY.ROOM_TIMER(gameId), - 'timer', - 'EX', - (parseInt(quiz.limitTime) + 3).toString(), - 'NX' - ); // 타이머 설정 - this.logger.verbose(`startQuizTime: ${gameId} - ${newQuizNum}`); - } - } - }); - - const roomSubscriber = this.redis.duplicate(); - await roomSubscriber.psubscribe('__keyspace@0__:Room:*'); - roomSubscriber.on('pmessage', async (_pattern, channel, message) => { - const key = channel.replace('__keyspace@0__:', ''); - const splitKey = key.split(':'); - if (splitKey.length !== 2) { - return; - } - const gameId = splitKey[1]; - - if (message === 'hset') { - const changes = await this.redis.get(`${key}:Changes`); - const roomData = await this.redis.hgetall(key); - - if (changes === 'Option') { - server.to(gameId).emit(SocketEvents.UPDATE_ROOM_OPTION, { - title: roomData.title, - gameMode: roomData.gameMode, - maxPlayerCount: roomData.maxPlayerCount, - isPublic: roomData.isPublic - }); - } else if (changes === 'Quizset') { - server.to(gameId).emit(SocketEvents.UPDATE_ROOM_QUIZSET, { - quizSetId: roomData.quizSetId, - quizCount: roomData.quizCount - }); - } else if (changes === 'Start') { - server.to(gameId).emit(SocketEvents.START_GAME, ''); - } - } - }); - - const playerSubscriber = this.redis.duplicate(); - playerSubscriber.psubscribe('__keyspace@0__:Player:*'); - playerSubscriber.on('pmessage', async (_pattern, channel, message) => { - const key = channel.replace('__keyspace@0__:', ''); - const splitKey = key.split(':'); - if (splitKey.length !== 2) { - return; - } - const playerId = splitKey[1]; - - if (message === 'hset') { - const changes = await this.redis.get(`${key}:Changes`); - const playerData = await this.redis.hgetall(key); - if (changes === 'Join') { - const newPlayer = { - playerId: playerId, - playerName: playerData.playerName, - playerPosition: [parseFloat(playerData.positionX), parseFloat(playerData.positionY)] - }; - server.to(playerData.gameId).emit(SocketEvents.JOIN_ROOM, { - players: [newPlayer] - }); - } else if (changes === 'Position') { - server.to(playerData.gameId).emit(SocketEvents.UPDATE_POSITION, { - playerId: playerId, - playerPosition: [parseFloat(playerData.positionX), parseFloat(playerData.positionY)] - }); - } else if (changes === 'Disconnect') { - server.to(playerData.gameId).emit(SocketEvents.EXIT_ROOM, { - playerId: playerId - }); - } - } - }); + await this.redisSubscriberService.initializeSubscribers(server); } async disconnect(clientId: string) { diff --git a/BE/test/integration/game.integration.spec.ts b/BE/test/integration/game.integration.spec.ts index 284dc5a..df8d003 100644 --- a/BE/test/integration/game.integration.spec.ts +++ b/BE/test/integration/game.integration.spec.ts @@ -27,6 +27,11 @@ import { QuizSetReadService } from '../../src/quiz-set/service/quiz-set-read.ser import { QuizSetUpdateService } from '../../src/quiz-set/service/quiz-set-update.service'; import { QuizSetDeleteService } from '../../src/quiz-set/service/quiz-set-delete.service'; import { ExceptionMessage } from '../../src/common/constants/exception-message'; +import { RedisSubscriberService } from '../../src/common/redis/redis-subscriber.service'; +import { ScoringSubscriber } from '../../src/common/redis/subscribers/scoring.subscriber'; +import { TimerSubscriber } from '../../src/common/redis/subscribers/timer.subscriber'; +import { RoomSubscriber } from '../../src/common/redis/subscribers/room.subscriber'; +import { PlayerSubscriber } from '../../src/common/redis/subscribers/player.subscriber'; /*disable eslint*/ @@ -95,6 +100,11 @@ describe('GameGateway (e2e)', () => { QuizSetReadService, QuizSetUpdateService, QuizSetDeleteService, + RedisSubscriberService, + ScoringSubscriber, + TimerSubscriber, + RoomSubscriber, + PlayerSubscriber, { provide: 'default_IORedisModuleConnectionToken', useValue: redisMock