Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,62 +1,39 @@
import {
WebSocketGateway,
WebSocketServer,
OnGatewayConnection,
OnGatewayDisconnect,
SubscribeMessage,
OnGatewayInit,
MessageBody,
ConnectedSocket,
} from "@nestjs/websockets";
import "dotenv/config";
import { Server, Socket } from "socket.io";
import { RoomService } from "../services/room.service";
import { Socket } from "socket.io";
import { RoomService } from "./services/room.service";
import { Logger, UsePipes, ValidationPipe } from "@nestjs/common";
import { EMIT_EVENT, LISTEN_EVENT } from "@/room/socket/room-socket.events";
import { EMIT_EVENT, LISTEN_EVENT } from "@/room/room.events";
import { CreateRoomDto } from "@/room/dto/create-room.dto";
import { RoomSocketService } from "@/room/socket/room-socket.service";
import { WebsocketService } from "@/websocket/websocket.service";
import { JoinRoomDto } from "@/room/dto/join-room.dto";
import { RoomRepository } from "@/room/room.repository";
import { ReactionDto } from "@/room/dto/reaction.dto";
import { RoomSocketRepository } from "@/room/socket/room-socket.repository";
import Redis from "ioredis";
import { createAdapter } from "@socket.io/redis-adapter";
import { RoomLeaveService } from "@/room/services/room-leave.service";
import { RoomCreateService } from "@/room/services/room-create.service";
import { RoomJoinService } from "@/room/services/room-join.service";

@WebSocketGateway()
export class RoomSocketGateway implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit {
@WebSocketServer()
private server: Server;
export class RoomGateway implements OnGatewayDisconnect {
private logger: Logger = new Logger("Room Gateway");

public constructor(
private readonly roomService: RoomService,
private readonly socketService: RoomSocketService,
private readonly socketRepository: RoomSocketRepository,
private readonly roomLeaveService: RoomLeaveService,
private readonly roomCreateService: RoomCreateService,
private readonly roomJoinService: RoomJoinService,
private readonly socketService: WebsocketService,
private readonly roomRepository: RoomRepository
) {}

public async handleConnection(client: Socket) {
Logger.log(`Client connected: ${client.id}`);
await this.socketRepository.register(client);
}

public async handleDisconnect(client: Socket) {
Logger.log(`Client disconnected: ${client.id}`);
await this.handleLeaveRoom(client);
await this.socketRepository.clean(client);
}

public async afterInit() {
const pubClient = new Redis({
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT),
});

const subClient = pubClient.duplicate();

const redisAdapter = createAdapter(pubClient, subClient);
this.server.adapter(redisAdapter);

this.roomService.setServer(this.server);
}

@SubscribeMessage(LISTEN_EVENT.CREATE)
Expand All @@ -65,7 +42,8 @@ export class RoomSocketGateway implements OnGatewayConnection, OnGatewayDisconne
@ConnectedSocket() client: Socket,
@MessageBody() dto: CreateRoomDto
) {
await this.roomService.createRoom({ ...dto, socketId: client.id });
// TODO: try - catch 로 에러 핸들링을 통해 이벤트 Emit 을 여기서 하기
await this.roomCreateService.createRoom({ ...dto, socketId: client.id });
}

@SubscribeMessage(LISTEN_EVENT.JOIN)
Expand All @@ -74,12 +52,12 @@ export class RoomSocketGateway implements OnGatewayConnection, OnGatewayDisconne
@ConnectedSocket() client: Socket,
@MessageBody() dto: JoinRoomDto
) {
await this.roomService.joinRoom({ ...dto, socketId: client.id });
await this.roomJoinService.joinRoom({ ...dto, socketId: client.id });
}

@SubscribeMessage(LISTEN_EVENT.LEAVE)
public async handleLeaveRoom(client: Socket) {
await this.roomService.leaveRoom(client);
await this.roomLeaveService.leaveRoom(client);
}

@SubscribeMessage(LISTEN_EVENT.FINISH)
Expand Down
16 changes: 8 additions & 8 deletions backend/src/room/room.module.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { Module } from "@nestjs/common";
import { RoomService } from "./services/room.service";
import { RoomSocketGateway } from "./socket/room-socket.gateway";
import { RoomGateway } from "./room.gateway";
import { RoomRepository } from "./room.repository";
import { RoomController } from "./room.controller";
import { RedisOmModule } from "nestjs-redis-om";
import { RoomEntity } from "./room.entity";
import { RoomLeaveService } from "@/room/services/room-leave.service";
import { RoomSocketService } from "@/room/socket/room-socket.service";
import { RoomHostService } from "@/room/services/room-host.service";
import { RoomSocketRepository } from "@/room/socket/room-socket.repository";
import { RoomSocketEntity } from "@/room/socket/room-socket.entity";
import { QuestionListRepository } from "@/question-list/question-list.repository";
import { WebsocketModule } from "@/websocket/websocket.module";
import { RoomCreateService } from "@/room/services/room-create.service";
import { RoomJoinService } from "@/room/services/room-join.service";

@Module({
imports: [RedisOmModule.forFeature([RoomEntity, RoomSocketEntity])],
imports: [RedisOmModule.forFeature([RoomEntity]), WebsocketModule],
providers: [
RoomService,
RoomSocketGateway,
RoomGateway,
RoomRepository,
RoomCreateService,
RoomJoinService,
RoomLeaveService,
RoomSocketService,
RoomHostService,
RoomSocketRepository,
QuestionListRepository,
],
controllers: [RoomController],
Expand Down
63 changes: 63 additions & 0 deletions backend/src/room/services/room-create.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Injectable } from "@nestjs/common";
import { CreateRoomInternalDto } from "@/room/dto/create-room.dto";
import { EMIT_EVENT } from "@/room/room.events";
import { WebsocketService } from "@/websocket/websocket.service";
import { RoomRepository } from "@/room/room.repository";
import { QuestionListRepository } from "@/question-list/question-list.repository";
import { RoomJoinService } from "@/room/services/room-join.service";
import { createHash } from "node:crypto";
import "dotenv/config";

@Injectable()
export class RoomCreateService {
private static ROOM_ID_CREATE_KEY = "room_id";

public constructor(
private readonly roomRepository: RoomRepository,
private readonly socketService: WebsocketService,
private readonly questionListRepository: QuestionListRepository,
private readonly roomJoinService: RoomJoinService
) {}

public async createRoom(dto: CreateRoomInternalDto) {
const { socketId, nickname } = dto;
const roomId = await this.generateRoomId();
const currentTime = Date.now();
const questionListContents = await this.questionListRepository.getContentsByQuestionListId(
dto.questionListId
);

const roomDto = {
...dto,
roomId,
connectionList: [],
questionListContents,
createdAt: currentTime,
host: dto.socketId,
};

await this.roomRepository.setRoom(roomDto);

await this.roomJoinService.joinRoom({ roomId, socketId, nickname }, true);

this.socketService.emitToRoom(roomId, EMIT_EVENT.CREATE, roomDto);
}

// TODO: 동시성 고려해봐야하지 않을까?
private async generateRoomId() {
const client = this.socketService.getRedisClient();

const idString = await client.get(RoomCreateService.ROOM_ID_CREATE_KEY);

let id: number;
if (idString && !isNaN(parseInt(idString))) {
id = await client.incr(RoomCreateService.ROOM_ID_CREATE_KEY);
} else {
id = parseInt(await client.set(RoomCreateService.ROOM_ID_CREATE_KEY, "1"));
}

return createHash("sha256")
.update(id + process.env.SESSION_HASH)
.digest("hex");
}
}
46 changes: 46 additions & 0 deletions backend/src/room/services/room-join.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Injectable } from "@nestjs/common";
import { EMIT_EVENT } from "@/room/room.events";
import { WebsocketService } from "@/websocket/websocket.service";
import { RoomRepository } from "@/room/room.repository";
import { RoomDto } from "@/room/dto/room.dto";
import { JoinRoomInternalDto } from "@/room/dto/join-room.dto";
import { WebsocketRepository } from "@/websocket/websocket.repository";

@Injectable()
export class RoomJoinService {
public constructor(
private readonly roomRepository: RoomRepository,
private readonly socketService: WebsocketService,
private readonly socketRepository: WebsocketRepository
) {}

public async joinRoom(dto: JoinRoomInternalDto, isCreate: boolean = false) {
const { roomId, socketId, nickname } = dto;

const room = await this.roomRepository.getRoom(roomId);
const socket = this.socketService.getSocket(socketId);

if (!socket) throw new Error("Invalid Socket");

if (room.roomId === null) throw new Error("Redis: RoomEntity Entity type error");

if (this.isFullRoom(room)) return socket.emit(EMIT_EVENT.FULL, {});

socket.join(roomId);
await this.socketRepository.joinRoom(socket.id, roomId);
room.connectionList.push({
socketId,
createAt: Date.now(),
nickname,
});

await this.roomRepository.setRoom(room);

// TODO: 성공 / 실패 여부를 전송하는데 있어서 결과에 따라 다르게 해야하는데.. 어떻게 관심 분리를 할까?
if (!isCreate) this.socketService.emitToRoom(roomId, EMIT_EVENT.JOIN, room);
}

private isFullRoom(room: RoomDto): boolean {
return room.maxParticipants <= room.connectionList.length;
}
}
28 changes: 11 additions & 17 deletions backend/src/room/services/room-leave.service.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { Injectable } from "@nestjs/common";
import { RoomSocketService } from "@/room/socket/room-socket.service";
import { WebsocketService } from "@/websocket/websocket.service";
import { RoomRepository } from "@/room/room.repository";
import { RoomDto } from "@/room/dto/room.dto";
import { RoomHostService } from "@/room/services/room-host.service";
import { EMIT_EVENT } from "@/room/socket/room-socket.events";
import { EMIT_EVENT } from "@/room/room.events";
import { Socket } from "socket.io";
import { RoomSocketRepository } from "@/room/socket/room-socket.repository";
import { WebsocketRepository } from "@/websocket/websocket.repository";

@Injectable()
export class RoomLeaveService {
constructor(
private readonly socketService: RoomSocketService,
private readonly socketRepository: RoomSocketRepository,
private readonly socketService: WebsocketService,
private readonly socketRepository: WebsocketRepository,
private readonly roomRepository: RoomRepository,
private readonly roomHostService: RoomHostService
) {}
Expand All @@ -27,23 +27,17 @@ export class RoomLeaveService {
const room = await this.roomRepository.getRoom(roomId);
if (!room) return;

await this.leaveSocket(socketId, room);

if (room.host === socketId) await this.handleHostChange(socketId, room);
else this.socketService.emitToRoom(room.roomId, EMIT_EVENT.QUIT, { socketId });

if (!room.connectionList.length) await this.deleteRoom(socketId);
}

private async leaveSocket(socketId: string, room: RoomDto) {
await this.socketService.leaveRoom(socketId, room.roomId);

// TODO : 엄청 비효율적인 코드라고 생각하는데 개선할 방법 찾기
room.connectionList = room.connectionList.filter(
(connection) => connection.socketId !== socketId
);

if (!room.connectionList.length) return this.deleteRoom(room.roomId);

await this.roomRepository.setRoom(room);

if (room.host === socketId) return this.handleHostChange(socketId, room);

this.socketService.emitToRoom(room.roomId, EMIT_EVENT.QUIT, { socketId });
}

private async handleHostChange(socketId: string, room: RoomDto) {
Expand Down
Loading