Skip to content

Commit

Permalink
Merge pull request #325 from boostcampwm-2024/release
Browse files Browse the repository at this point in the history
[๊ณตํ†ต] Release -> main
  • Loading branch information
NewCodes7 authored Dec 5, 2024
2 parents 1ed3388 + fd4a280 commit 4d25d9a
Show file tree
Hide file tree
Showing 79 changed files with 1,413 additions and 535 deletions.
5 changes: 3 additions & 2 deletions BE/src/InitDB/InitDB.controller.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Controller, Post } from '@nestjs/common';
import { InitDBService } from './InitDB.Service';
import { Controller, HttpException, Post } from '@nestjs/common';

@Controller('/api/initDB')
export class InitDBController {
constructor(private readonly initDBService: InitDBService) {}

@Post()
create() {
return this.initDBService.create();
throw new HttpException('test์šฉ api ์ž…๋‹ˆ๋‹ค.', 501);
// return this.initDBService.create();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ export enum GameMode {
RANKING = 'RANKING',
SURVIVAL = 'SURVIVAL',
}

export enum SurvivalStatus {
ALIVE = '1',
DEAD = '0',
}
2 changes: 1 addition & 1 deletion BE/src/game/dto/create-game.dto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IsIn, IsInt, IsString, Max, MaxLength, Min, MinLength } from 'class-validator';
import { Transform, Type } from 'class-transformer';
import { WsException } from '@nestjs/websockets';
import { GameMode } from '../../common/constants/game-mode';
import { GameMode } from '../../common/constants/game';

export class CreateGameDto {
@IsString()
Expand Down
2 changes: 1 addition & 1 deletion BE/src/game/dto/update-room-option.dto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IsIn, IsInt, IsString, Length, Max, MaxLength, Min, MinLength } from 'class-validator';
import { Transform, Type } from 'class-transformer';
import { WsException } from '@nestjs/websockets';
import { GameMode } from '../../common/constants/game-mode';
import { GameMode } from '../../common/constants/game';

export class UpdateRoomOptionDto {
@IsString()
Expand Down
3 changes: 2 additions & 1 deletion BE/src/game/game.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ import { ExceptionMessage } from '../common/constants/exception-message';
origin: [
'https://news.taskify.shop',
'https://quizground.duckdns.org',
'https://admin.socket.io'
'https://admin.socket.io',
'https://quizground.site'
],
credentials: true
},
Expand Down
55 changes: 53 additions & 2 deletions BE/src/game/redis/game-redis-memory.service.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRedis } from '@nestjs-modules/ioredis';
import Redis from 'ioredis';
import { Cron, CronExpression } from '@nestjs/schedule';
import { REDIS_KEY } from '../../common/constants/redis-key.constant';
import { Cron, CronExpression } from '@nestjs/schedule';

@Injectable()
export class GameRedisMemoryService {
private readonly logger = new Logger(GameRedisMemoryService.name);
private readonly BATCH_SIZE = 100; // ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•  ๋ฐฐ์น˜ ํฌ๊ธฐ
private readonly INACTIVE_THRESHOLD = 30 * 60 * 1000; // 30๋ถ„ 30 * 60 * 1000;

private readonly TTL = {
ROOM: 3 * 60 * 60,
Expand All @@ -18,11 +19,61 @@ export class GameRedisMemoryService {

constructor(@InjectRedis() private readonly redis: Redis) {}

/**
* ๋น„ํ™œ์„ฑ ๋ฐฉ ์ฒดํฌ (์ฃผ๊ธฐ์ ์œผ๋กœ ์‹คํ–‰)
*/
/**
* ๋น„ํ™œ์„ฑ ๋ฐฉ์„ ์ฒดํฌํ•˜๊ณ  ์ •๋ฆฌํ•˜๋Š” ํฌ๋ก  ์ž‘์—…
* SCAN์„ ์‚ฌ์šฉํ•˜์—ฌ ๋Œ€๊ทœ๋ชจ ๋ฐฉ ๋ชฉ๋ก๋„ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ
*/
@Cron(CronExpression.EVERY_10_MINUTES)
async checkInactiveRooms(): Promise<void> {
this.logger.verbose('๋น„ํ™œ์„ฑ ๋ฐฉ ์ฒดํฌ ์‹œ์ž‘');
try {
const now = Date.now();
let cursor = '0';
let processedCount = 0;

do {
// SCAN์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐฐ์น˜ ๋‹จ์œ„๋กœ ์ฒ˜๋ฆฌ
const [nextCursor, rooms] = await this.redis.sscan(
REDIS_KEY.ACTIVE_ROOMS,
cursor,
'COUNT',
this.BATCH_SIZE
);
cursor = nextCursor;

// ๋ณ‘๋ ฌ๋กœ ๋ฐฉ ์ƒํƒœ ์ฒดํฌ ๋ฐ ์ฒ˜๋ฆฌ
await Promise.all(
rooms.map(async (roomId) => {
try {
const lastActivity = await this.redis.hget(REDIS_KEY.ROOM(roomId), 'lastActivityAt');

if (lastActivity && now - parseInt(lastActivity) > this.INACTIVE_THRESHOLD) {
await this.redis.publish('room:cleanup', roomId);
this.logger.verbose(`๋น„ํ™œ์„ฑ์œผ๋กœ ์ธํ•ด ๋ฐฉ ${roomId} ์ •๋ฆฌ ์‹œ์ž‘`);
processedCount++;
}
} catch (error) {
this.logger.error(`๋ฐฉ ${roomId} ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${error.message}`);
}
})
);
} while (cursor !== '0');

this.logger.verbose(`๋น„ํ™œ์„ฑ ๋ฐฉ ์ฒดํฌ ์™„๋ฃŒ: ${processedCount}๊ฐœ ๋ฐฉ ์ •๋ฆฌ๋จ`);
} catch (error) {
this.logger.error(`๋น„ํ™œ์„ฑ ๋ฐฉ ์ฒดํฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${error.message}`);
}
}

/**
* TTL ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ ์Šค์ผ€์ค„๋Ÿฌ
* ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋กœ ๋ธ”๋กœํ‚น ์ตœ์†Œํ™”
*/
@Cron(CronExpression.EVERY_MINUTE)

// @Cron(CronExpression.EVERY_MINUTE)
async manageTTL(): Promise<void> {
try {
// SCAN์œผ๋กœ ํ™œ์„ฑ ๋ฐฉ ๋ชฉ๋ก์„ ๋ฐฐ์น˜๋กœ ์ฒ˜๋ฆฌ
Expand Down
38 changes: 23 additions & 15 deletions BE/src/game/redis/subscribers/player.subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Redis from 'ioredis';
import { Namespace } from 'socket.io';
import SocketEvents from '../../../common/constants/socket-events';
import { REDIS_KEY } from '../../../common/constants/redis-key.constant';
import { SurvivalStatus } from '../../../common/constants/game';

@Injectable()
export class PlayerSubscriber extends RedisSubscriber {
Expand Down Expand Up @@ -84,27 +85,34 @@ export class PlayerSubscriber extends RedisSubscriber {

const isAlivePlayer = await this.redis.hget(REDIS_KEY.PLAYER(playerId), 'isAlive');

if (isAlivePlayer === '1') {
if (isAlivePlayer === SurvivalStatus.ALIVE) {
server.to(gameId).emit(SocketEvents.UPDATE_POSITION, updateData);
} else if (isAlivePlayer === '0') {
} else if (isAlivePlayer === SurvivalStatus.DEAD) {
const players = await this.redis.smembers(REDIS_KEY.ROOM_PLAYERS(gameId));
const deadPlayers = await Promise.all(
players.map(async (id) => {
const isAlive = await this.redis.hget(REDIS_KEY.PLAYER(id), 'isAlive');
return { id, isAlive };
})
);

deadPlayers
const pipeline = this.redis.pipeline();

players.forEach((id) => {
pipeline.hmget(REDIS_KEY.PLAYER(id), 'isAlive', 'socketId');
});

type Result = [Error | null, [string, string] | null];
const results = await pipeline.exec();

(results as Result[])
.map(([err, data], index) => ({
id: players[index],
isAlive: err ? null : data?.[0],
socketId: err ? null : data?.[1]
}))
.filter((player) => player.isAlive === '0')
.forEach((player) => {
server.to(player.id).emit(SocketEvents.UPDATE_POSITION, updateData);
const socket = server.sockets.get(player.socketId);
if (!socket) {
return;
}
socket.emit(SocketEvents.UPDATE_POSITION, updateData);
});
}

this.logger.verbose(
`[updatePosition] RoomId: ${gameId} | playerId: ${playerId} | isAlive: ${isAlivePlayer === '1' ? '์ƒ์กด์ž' : '๊ด€์ „์ž'} | position: [${positionX}, ${positionY}]`
);
}

private async handlePlayerDisconnect(playerId: string, playerData: any, server: Namespace) {
Expand Down
9 changes: 9 additions & 0 deletions BE/src/game/redis/subscribers/room.cleanup.subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ export class RoomCleanupSubscriber extends RedisSubscriber {
try {
const pipeline = this.redis.pipeline();

// 1. ๋ฐฉ์— ์†ํ•œ ํ”Œ๋ ˆ์ด์–ด ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ, 200๋ช…๋ฏธ๋งŒ -> smembers ์‚ฌ์šฉ!
const players = await this.redis.smembers(REDIS_KEY.ROOM_PLAYERS(roomId));

// 2. ํ”Œ๋ ˆ์ด์–ด ๋ฐ์ดํ„ฐ ์‚ญ์ œ
for (const playerId of players) {
pipeline.del(REDIS_KEY.PLAYER(playerId)); // ํ”Œ๋ ˆ์ด์–ด ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ
pipeline.del(`${REDIS_KEY.PLAYER(playerId)}:Changes`); // ํ”Œ๋ ˆ์ด์–ด Changes ๋ฐ์ดํ„ฐ
}

// 1. ๋ฐฉ ๊ด€๋ จ ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ ์‚ญ์ œ
pipeline.del(REDIS_KEY.ROOM(roomId));
pipeline.del(REDIS_KEY.ROOM_PLAYERS(roomId));
Expand Down
48 changes: 27 additions & 21 deletions BE/src/game/redis/subscribers/timer.subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Redis from 'ioredis';
import { Namespace } from 'socket.io';
import { REDIS_KEY } from '../../../common/constants/redis-key.constant';
import SocketEvents from '../../../common/constants/socket-events';
import { GameMode } from '../../../common/constants/game-mode';
import { GameMode, SurvivalStatus } from '../../../common/constants/game';

@Injectable()
export class TimerSubscriber extends RedisSubscriber {
Expand All @@ -28,6 +28,7 @@ export class TimerSubscriber extends RedisSubscriber {
const currentQuiz = await this.redis.get(REDIS_KEY.ROOM_CURRENT_QUIZ(gameId));
const [quizNum, state] = currentQuiz.split(':');

// REFACTOR: start, end ์ƒ์ˆ˜ํ™”
if (state === 'start') {
await this.handleQuizScoring(gameId, parseInt(quizNum), server);
} else {
Expand Down Expand Up @@ -65,7 +66,7 @@ export class TimerSubscriber extends RedisSubscriber {
const selectAnswer = this.calculateAnswer(
player.positionX,
player.positionY,
quizList.length
parseInt(quiz.choiceCount)
);
// this.logger.verbose(selectAnswer);

Expand Down Expand Up @@ -113,12 +114,18 @@ export class TimerSubscriber extends RedisSubscriber {

// ์ƒ์กด ๋ชจ๋“œ์—์„œ ๋ชจ๋‘ ํƒˆ๋ฝํ•˜์ง„ ์•Š์•˜๋Š”์ง€ ์ฒดํฌ
const players = await this.redis.smembers(REDIS_KEY.ROOM_PLAYERS(gameId));
const alivePlayers = players.filter(async (id) => {
const isAlive = await this.redis.hget(REDIS_KEY.PLAYER(id), 'isAlive');
return isAlive === '1';
});
const aliveCount = (
await Promise.all(players.map((id) => this.redis.hget(REDIS_KEY.PLAYER(id), 'isAlive')))
).filter((isAlive) => isAlive === SurvivalStatus.ALIVE).length;

// ๊ฒŒ์ž„ ๋์„ ์•Œ๋ฆผ
if (this.hasNoMoreQuiz(quizList, newQuizNum) || this.checkSurvivalEnd(players.length, aliveCount)) {
// ๋ชจ๋“  ํ”Œ๋ ˆ์ด์–ด๋ฅผ ์ƒ์กด์ž๋กœ ๋ณ€๊ฒฝ
const players = await this.redis.smembers(REDIS_KEY.ROOM_PLAYERS(gameId));
players.forEach((id) => {
this.redis.hset(REDIS_KEY.PLAYER(id), { isAlive: SurvivalStatus.ALIVE });
});

if (quizList.length <= newQuizNum || alivePlayers.length === 0) {
const leaderboard = await this.redis.zrange(
REDIS_KEY.ROOM_LEADERBOARD(gameId),
0,
Expand All @@ -128,13 +135,13 @@ export class TimerSubscriber extends RedisSubscriber {

this.redis.set(`${REDIS_KEY.ROOM(gameId)}:Changes`, 'End');
this.redis.hset(REDIS_KEY.ROOM(gameId), {
host: leaderboard[0],
host: leaderboard.at(-2),
status: 'waiting',
isWaiting: '1'
});

server.to(gameId).emit(SocketEvents.END_GAME, {
hostId: leaderboard[0]
hostId: leaderboard.at(-2)
});
this.logger.verbose(`[endGame]: ${gameId}`);
return;
Expand Down Expand Up @@ -167,8 +174,8 @@ export class TimerSubscriber extends RedisSubscriber {
}

private calculateAnswer(positionX: string, positionY: string, quizLen: number): number {
const x = parseFloat(positionX);
const y = parseFloat(positionY);
const x = parseFloat(positionY);
const y = parseFloat(positionX);

// ํ–‰์˜ ๊ฐœ์ˆ˜ ๊ณ„์‚ฐ (2์—ด ๊ณ ์ •์ด๋ฏ€๋กœ ์ด ๊ฐœ์ˆ˜์˜ ์ ˆ๋ฐ˜์„ ์˜ฌ๋ฆผ)
const rows = Math.ceil(quizLen / 2);
Expand All @@ -177,21 +184,20 @@ export class TimerSubscriber extends RedisSubscriber {
const rowIndex = Math.floor(y * rows);

// X ์ขŒํ‘œ๋กœ ์™ผ์ชฝ/์˜ค๋ฅธ์ชฝ ๊ฒฐ์ •
const colIndex = x < 0.5 ? 0 : 1;
const colIndex = Math.round(x);

// ์ตœ์ข… ์„ ํƒ์ง€ ๋ฒˆํ˜ธ ๊ณ„์‚ฐ
const answer = rowIndex * 2 + colIndex + 1;

// ์‹ค์ œ ์„ ํƒ์ง€ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๋„๋ก ๋ณด์ •
return Math.min(answer, quizLen);
// return (
// Math.round(
// parseFloat(positionX) + Math.floor(parseFloat(positionY) * Math.ceil(quizLen / 2))
// ) * 2
// );
// if (parseFloat(positionY) < 0.5) {
// return parseFloat(positionX) < 0.5 ? 1 : 2;
// }
// return parseFloat(positionX) < 0.5 ? 3 : 4;
}

private hasNoMoreQuiz(quizList, newQuizNum: number) {
return quizList.length <= newQuizNum;
}

private checkSurvivalEnd(playerCount: number, aliveCount: number) {
return playerCount > 1 && aliveCount <= 1;
}
}
17 changes: 12 additions & 5 deletions BE/src/game/service/game.chat.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { REDIS_KEY } from '../../common/constants/redis-key.constant';
import SocketEvents from '../../common/constants/socket-events';
import { Namespace } from 'socket.io';
import { TraceClass } from '../../common/interceptor/SocketEventLoggerInterceptor';
import { SurvivalStatus } from '../../common/constants/game';

@TraceClass()
@Injectable()
Expand Down Expand Up @@ -56,7 +57,8 @@ export class GameChatService {
const playerKey = REDIS_KEY.PLAYER(chatMessage.playerId);
const isAlivePlayer = await this.redis.hget(playerKey, 'isAlive');

if (isAlivePlayer === '1') {
// ์ƒ์กดํ•œ ์‚ฌ๋žŒ์ด๋ผ๋ฉด ์ „์ฒด ๋ธŒ๋กœ๋“œ์บ์ŠคํŒ…
if (isAlivePlayer === SurvivalStatus.ALIVE) {
server.to(gameId).emit(SocketEvents.CHAT_MESSAGE, chatMessage);
return;
}
Expand All @@ -65,11 +67,16 @@ export class GameChatService {
const players = await this.redis.smembers(REDIS_KEY.ROOM_PLAYERS(gameId));
await Promise.all(
players.map(async (playerId) => {
const playerKey = REDIS_KEY.PLAYER(playerId);
const isAlive = await this.redis.hget(playerKey, 'isAlive');
const socketId = await this.redis.hget(REDIS_KEY.PLAYER(playerId), 'socketId');
const socket = server.sockets.get(socketId);

if (isAlive === '0') {
server.to(playerId).emit(SocketEvents.CHAT_MESSAGE, chatMessage);
if (!socket) {
return;
}

const isAlive = await this.redis.hget(REDIS_KEY.PLAYER(playerId), 'isAlive');
if (isAlive === SurvivalStatus.DEAD) {
socket.emit(SocketEvents.CHAT_MESSAGE, chatMessage);
}
})
);
Expand Down
Loading

0 comments on commit 4d25d9a

Please sign in to comment.