diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 93dd2b4b..33f617dd 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -10,10 +10,11 @@ import { TypeOrmModule } from "@nestjs/typeorm"; import "dotenv/config"; -import { createDataSource, typeOrmConfig } from "./config/typeorm.config"; +import { createDataSource, typeOrmConfig } from "./infra/typeorm/typeorm.config"; import { QuestionListModule } from "./question-list/question-list.module"; import { RedisOmModule } from "@moozeh/nestjs-redis-om"; import { SigServerModule } from "@/signaling-server/sig-server.module"; +import { redisConfig } from "@/infra/infra.config"; @Module({ imports: [ @@ -22,7 +23,7 @@ import { SigServerModule } from "@/signaling-server/sig-server.module"; dataSourceFactory: async () => await createDataSource(), // 분리된 데이터소스 생성 함수 사용 }), RedisOmModule.forRoot({ - url: `redis://${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`, + url: `redis://${redisConfig.host}:${redisConfig.port}`, }), RoomModule, AuthModule, diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index 84ef8c21..d5d3e46d 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -9,7 +9,7 @@ import { } from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; import { Response } from "express"; -import { setCookieConfig } from "@/config/cookie.config"; +import { setCookieConfig } from "@/infra/cookie/cookie.config"; import { JwtPayload, JwtTokenPair } from "./jwt/jwt.decorator"; import { IJwtPayload, IJwtToken, IJwtTokenPair } from "./jwt/jwt.model"; diff --git a/backend/src/config/cookie.config.ts b/backend/src/infra/cookie/cookie.config.ts similarity index 100% rename from backend/src/config/cookie.config.ts rename to backend/src/infra/cookie/cookie.config.ts diff --git a/backend/src/config/redis.config.ts b/backend/src/infra/infra.config.ts similarity index 64% rename from backend/src/config/redis.config.ts rename to backend/src/infra/infra.config.ts index 0b97e561..34bf61d9 100644 --- a/backend/src/config/redis.config.ts +++ b/backend/src/infra/infra.config.ts @@ -1,5 +1,11 @@ import "dotenv/config"; +export const gatewayConfig = { + cors: { + origin: process.env.DOMAIN || "*", + }, +}; + export const redisConfig = { host: process.env.REDIS_HOST, port: parseInt(process.env.REDIS_PORT), diff --git a/backend/src/infra/infra.module.ts b/backend/src/infra/infra.module.ts new file mode 100644 index 00000000..398482f8 --- /dev/null +++ b/backend/src/infra/infra.module.ts @@ -0,0 +1,14 @@ +import { Global, Module } from "@nestjs/common"; +import { InfraService } from "@/infra/infra.service"; +import { WebsocketRepository } from "@/infra/websocket/websocket.repository"; +import { RedisOmModule } from "@moozeh/nestjs-redis-om"; +import { RoomEntity } from "@/room/room.entity"; +import { WebsocketEntity } from "@/infra/websocket/websocket.entity"; + +@Global() +@Module({ + imports: [RedisOmModule.forFeature([RoomEntity, WebsocketEntity])], + providers: [InfraService, WebsocketRepository], + exports: [InfraService, WebsocketRepository], +}) +export class InfraModule {} diff --git a/backend/src/infra/infra.service.ts b/backend/src/infra/infra.service.ts new file mode 100644 index 00000000..ebf8a866 --- /dev/null +++ b/backend/src/infra/infra.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from "@nestjs/common"; +import { Server, Socket } from "socket.io"; +import Redis from "ioredis"; +import { redisClient } from "@/infra/redis/redis-client"; +import { WebsocketRepository } from "@/infra/websocket/websocket.repository"; + +@Injectable() +export class InfraService { + private server: Server; + private readonly redisClient: Redis = redisClient; + + constructor(private readonly websocketRepository: WebsocketRepository) {} + + public setServer(server: Server) { + this.server = server; + } + + public async createSocketMetadata(socket: Socket) { + return this.websocketRepository.createWebsocketMetadata(socket); + } + + public async getSocketMetadata(socket: Socket) { + return this.websocketRepository.getWebsocketMetadataById(socket.id); + } + + public async removeSocketMetadata(socket: Socket) { + await this.websocketRepository.removeWebsocketMetadata(socket); + } + + public async joinRoom(socket: Socket, roomId: string) { + const entity = await this.websocketRepository.getWebsocketMetadataById(socket.id); + entity.joinedRooms.push(roomId); + await this.websocketRepository.updateWebsocketMetadata(socket.id, entity); + socket.join(roomId); + } + + public getRedisClient() { + return this.redisClient; + } + + public getServer() { + return this.server; + } + + public getSocket(socketId: string) { + return this.server.sockets.sockets.get(socketId); + } + + public emitToRoom(roomId: string, event: string, data?: any) { + this.server.to(roomId).emit(event, data); + } +} diff --git a/backend/src/infra/redis/redis-client.ts b/backend/src/infra/redis/redis-client.ts new file mode 100644 index 00000000..91b0b9ee --- /dev/null +++ b/backend/src/infra/redis/redis-client.ts @@ -0,0 +1,3 @@ +import Redis from "ioredis"; + +export const redisClient = new Redis(); diff --git a/backend/src/config/typeorm.config.ts b/backend/src/infra/typeorm/typeorm.config.ts similarity index 100% rename from backend/src/config/typeorm.config.ts rename to backend/src/infra/typeorm/typeorm.config.ts diff --git a/backend/src/websocket/websocket.entity.ts b/backend/src/infra/websocket/websocket.entity.ts similarity index 100% rename from backend/src/websocket/websocket.entity.ts rename to backend/src/infra/websocket/websocket.entity.ts diff --git a/backend/src/websocket/websocket.repository.ts b/backend/src/infra/websocket/websocket.repository.ts similarity index 60% rename from backend/src/websocket/websocket.repository.ts rename to backend/src/infra/websocket/websocket.repository.ts index e2fa3ee1..f633492f 100644 --- a/backend/src/websocket/websocket.repository.ts +++ b/backend/src/infra/websocket/websocket.repository.ts @@ -2,7 +2,7 @@ import { Injectable } from "@nestjs/common"; import { Socket } from "socket.io"; import { InjectRepository } from "@moozeh/nestjs-redis-om"; import { Repository } from "redis-om"; -import { WebsocketEntity } from "@/websocket/websocket.entity"; +import { WebsocketEntity } from "@/infra/websocket/websocket.entity"; @Injectable() export class WebsocketRepository { @@ -11,19 +11,18 @@ export class WebsocketRepository { private readonly socketRepository: Repository ) {} - public async register(socket: Socket) { + public async createWebsocketMetadata(socket: Socket) { const entity = new WebsocketEntity(); entity.id = socket.id; entity.joinedRooms = []; - await this.socketRepository.remove(entity.id); - await this.socketRepository.save(entity.id, entity); + return this.socketRepository.save(entity.id, entity); } - public async getRoomOfSocket(id: string) { + public async getWebsocketMetadataById(id: string) { return this.socketRepository.search().where("id").eq(id).return.first(); } - public async clean(socket: Socket) { + public async removeWebsocketMetadata(socket: Socket) { const entities = await this.socketRepository .search() .where("id") @@ -35,9 +34,7 @@ export class WebsocketRepository { } } - public async joinRoom(socketId: string, roomId: string) { - const entity = await this.socketRepository.search().where("id").eq(socketId).return.first(); - entity.joinedRooms.push(roomId); - await this.socketRepository.save(entity.id, entity); + public async updateWebsocketMetadata(socketId: string, entity: WebsocketEntity) { + return this.socketRepository.save(entity.id, entity); } } diff --git a/backend/src/room/domain/room.ts b/backend/src/room/domain/room.ts new file mode 100644 index 00000000..39a7af0c --- /dev/null +++ b/backend/src/room/domain/room.ts @@ -0,0 +1,115 @@ +import { RoomEntity } from "@/room/room.entity"; + +export interface Connection { + socketId: string; + nickname: string; + createAt: number; +} + +export enum RoomStatus { + PUBLIC = "PUBLIC", + PRIVATE = "PRIVATE", +} + +export interface IRoom { + id: string; + title: string; + category: string[]; + inProgress: boolean; + currentIndex: number; + host: Connection; + status: RoomStatus; + maxParticipants: number; + maxQuestionListLength: number; + questionListId: number; + questionListTitle: string; + createdAt: number; + connectionMap: Record; +} + +export class Room { + public entity: RoomEntity; + + constructor(entity?: IRoom) { + if (!entity) { + this.entity = null; + return this; + } + this.entity = new RoomEntity(); + this.entity.id = entity.id; + this.entity.status = entity.status; + this.setConnection(entity.connectionMap); + this.entity.title = entity.title; + this.entity.createdAt = entity.createdAt; + this.entity.questionListId = entity.questionListId; + this.entity.questionListTitle = entity.questionListTitle; + this.entity.inProgress = entity.inProgress; + this.entity.maxParticipants = entity.maxParticipants; + this.entity.currentIndex = entity.currentIndex; + this.entity.category = entity.category; + this.entity.maxQuestionListLength = entity.maxQuestionListLength; + this.entity.host = JSON.stringify(entity.host); + } + + static fromEntity(entity: RoomEntity) { + const room = new Room(); + room.entity = entity; + return room; + } + + build() { + return this.entity; + } + + toObject(): IRoom { + return { + id: this.entity.id, + status: this.entity.status, + connectionMap: this.getConnection(), + title: this.entity.title, + createdAt: this.entity.createdAt, + questionListId: this.entity.questionListId, + questionListTitle: this.entity.questionListTitle, + inProgress: this.entity.inProgress, + maxParticipants: this.entity.maxParticipants, + currentIndex: this.entity.currentIndex, + category: this.entity.category, + maxQuestionListLength: this.entity.maxQuestionListLength, + host: this.getHost(), + }; + } + + setHost(connection: Connection) { + this.entity.host = JSON.stringify(connection); + return this; + } + + getHost(): Connection { + return JSON.parse(this.entity.host); + } + + setConnection(connectionMap: Record) { + this.entity.connectionMap = JSON.stringify(connectionMap); + return this; + } + + getConnection(): Record { + return JSON.parse(this.entity.connectionMap); + } + + getParticipants(): number { + return Object.keys(this.getConnection()).length; + } + + addConnection(connection: Connection) { + const connectionMap = this.getConnection(); + connectionMap[connection.socketId] = connection; + return this.setConnection(connectionMap); + } + + removeConnection(socketId: string) { + const connectionMap = this.getConnection(); + delete connectionMap[socketId]; + return this.setConnection(connectionMap); + } +} diff --git a/backend/src/room/dto/all-room.dto.ts b/backend/src/room/dto/all-room.dto.ts index 89c8b104..288aa4de 100644 --- a/backend/src/room/dto/all-room.dto.ts +++ b/backend/src/room/dto/all-room.dto.ts @@ -1,6 +1,7 @@ // DTO 클래스 정의 import { Transform } from "class-transformer"; import { IsBoolean, IsOptional } from "class-validator"; +import { Connection, RoomStatus } from "@/room/domain/room"; export class AllRoomQueryParamDto { @IsOptional() @@ -12,3 +13,16 @@ export class AllRoomQueryParamDto { @IsBoolean() inProgress: boolean; } + +export class RoomListResponseDto { + createdAt: number; + host: Connection; + maxParticipants: number; + status: RoomStatus; + title: string; + id: string; + category: string[]; + inProgress: boolean; + questionListTitle: string; + participants: number; +} diff --git a/backend/src/room/dto/create-room.dto.ts b/backend/src/room/dto/create-room.dto.ts index 04e6a772..ca24e6c5 100644 --- a/backend/src/room/dto/create-room.dto.ts +++ b/backend/src/room/dto/create-room.dto.ts @@ -8,9 +8,14 @@ import { Max, Min, } from "class-validator"; -import { RoomStatus } from "@/room/room.entity"; +import { Connection, RoomStatus } from "@/room/domain/room"; +import { Question } from "@/question-list/entity/question.entity"; export class CreateRoomDto { + private static MAX_CATEGORY_SIZE = 3; + private static MIN_CATEGORY_SIZE = 1; + private static MAX_PARTICIPANT_SIZE = 5; + private static MIN_PARTICIPANT_SIZE = 1; @IsNotEmpty() title: string; @@ -24,45 +29,32 @@ export class CreateRoomDto { @IsNotEmpty() @IsArray() - @ArrayMaxSize(3) - @ArrayMinSize(1) + @ArrayMaxSize(CreateRoomDto.MAX_CATEGORY_SIZE) + @ArrayMinSize(CreateRoomDto.MIN_CATEGORY_SIZE) category: string[]; @IsNumber() - @Min(1) - @Max(5) + @Min(CreateRoomDto.MIN_PARTICIPANT_SIZE) + @Max(CreateRoomDto.MAX_PARTICIPANT_SIZE) maxParticipants: number; @IsNumber() questionListId: number; } -export class CreateRoomInternalDto { - @IsNotEmpty() +export interface CreateRoomResponseDto { title: string; - - @IsEnum(RoomStatus, { - message: "Status must be either PUBLIC or PRIVATE", - }) status: RoomStatus; - - @IsNotEmpty() nickname: string; - - @IsNotEmpty() - @IsArray() - @ArrayMaxSize(3) - @ArrayMinSize(1) - category: string[]; - - @IsNotEmpty() - socketId: string; - - @IsNumber() - @Min(1) - @Max(5) maxParticipants: number; - - @IsNumber() + category: string[]; questionListId: number; + socketId: string; + id: string; + inProgress: boolean; + connectionMap: Record; + participants: number; + questionListContents: Question[]; + createdAt: number; + host: Connection; } diff --git a/backend/src/room/dto/index.ts b/backend/src/room/dto/index.ts new file mode 100644 index 00000000..52119319 --- /dev/null +++ b/backend/src/room/dto/index.ts @@ -0,0 +1,7 @@ +export * from "./create-room.dto"; +export * from "./finish-room.dto"; +export * from "./join-room.dto"; +export * from "./move-index.dto"; +export * from "./reaction.dto"; +export * from "./room-id.dto"; +export * from "./all-room.dto"; diff --git a/backend/src/room/dto/join-room.dto.ts b/backend/src/room/dto/join-room.dto.ts index 28ba0c06..8df0b0fc 100644 --- a/backend/src/room/dto/join-room.dto.ts +++ b/backend/src/room/dto/join-room.dto.ts @@ -1,4 +1,6 @@ import { IsNotEmpty } from "class-validator"; +import { Question } from "@/question-list/entity/question.entity"; +import { Connection, RoomStatus } from "@/room/domain/room"; export class JoinRoomDto { @IsNotEmpty() @@ -8,13 +10,19 @@ export class JoinRoomDto { nickname: string; } -export class JoinRoomInternalDto { - @IsNotEmpty() - roomId: string; - - @IsNotEmpty() - socketId: string; - - @IsNotEmpty() - nickname: string; +export interface JoinRoomResponseDto { + category: string[]; + inProgress: boolean; + connectionMap: Record; + createdAt: number; + currentIndex: number; + maxQuestionListLength: number; + questionListId: number; + host: Connection; + participants: number; + maxParticipants: number; + status: RoomStatus; + title: string; + id: string; + questionListContents: Question[]; } diff --git a/backend/src/room/dto/room-list.dto.ts b/backend/src/room/dto/room-list.dto.ts deleted file mode 100644 index adee64a7..00000000 --- a/backend/src/room/dto/room-list.dto.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Connection, RoomStatus } from "@/room/room.entity"; -import { Max, Min } from "class-validator"; - -export class RoomList { - createdAt: number; - - host: Connection; - - @Max(5) - @Min(1) - maxParticipants: number; - - status: RoomStatus; - - title: string; - - id: string; - - category: string[]; - - inProgress: boolean; - - questionListTitle: string; - - @Max(5) - @Min(1) - participants: number; -} - -export type RoomListResponseDto = RoomList[]; diff --git a/backend/src/room/dto/room.dto.ts b/backend/src/room/dto/room.dto.ts deleted file mode 100644 index c6949fe2..00000000 --- a/backend/src/room/dto/room.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Connection, RoomStatus } from "@/room/room.entity"; - -export interface RoomDto { - id: string; - title: string; - category: string[]; - inProgress: boolean; - currentIndex: number; - host: Connection; - status: RoomStatus; - participants: number; - maxParticipants: number; - maxQuestionListLength: number; - questionListId: number; - questionListTitle: string; - createdAt: number; - connectionMap: Record; -} diff --git a/backend/src/room/exceptions/join-room-exceptions.ts b/backend/src/room/exceptions/join-room-exceptions.ts new file mode 100644 index 00000000..5c0b01d7 --- /dev/null +++ b/backend/src/room/exceptions/join-room-exceptions.ts @@ -0,0 +1,7 @@ +export class FullRoomException extends Error { + name: "FullRoomException"; +} + +export class InProgressException extends Error { + name: "InProgressException"; +} diff --git a/backend/src/room/room.controller.ts b/backend/src/room/room.controller.ts index a7c78494..284b97b3 100644 --- a/backend/src/room/room.controller.ts +++ b/backend/src/room/room.controller.ts @@ -1,6 +1,6 @@ import { Controller, Get, Query, UsePipes, ValidationPipe } from "@nestjs/common"; -import { RoomService } from "./services/room.service"; -import { AllRoomQueryParamDto } from "@/room/dto/all-room.dto"; +import { RoomService } from "./room.service"; +import { AllRoomQueryParamDto } from "@/room/dto"; @Controller("rooms") export class RoomController { diff --git a/backend/src/room/room.entity.ts b/backend/src/room/room.entity.ts index 569f0738..005cca7a 100644 --- a/backend/src/room/room.entity.ts +++ b/backend/src/room/room.entity.ts @@ -1,15 +1,5 @@ import { Entity, Field, Schema } from "@moozeh/nestjs-redis-om"; - -export interface Connection { - socketId: string; - nickname: string; - createAt: number; -} - -export enum RoomStatus { - PUBLIC = "PUBLIC", - PRIVATE = "PRIVATE", -} +import { RoomStatus } from "@/room/domain/room"; @Schema("room", {}) export class RoomEntity extends Entity { diff --git a/backend/src/room/room.gateway.ts b/backend/src/room/room.gateway.ts index cce3c73e..63d64508 100644 --- a/backend/src/room/room.gateway.ts +++ b/backend/src/room/room.gateway.ts @@ -1,43 +1,63 @@ import { ConnectedSocket, MessageBody, + OnGatewayConnection, OnGatewayDisconnect, + OnGatewayInit, SubscribeMessage, WebSocketGateway, + WebSocketServer, } from "@nestjs/websockets"; import "dotenv/config"; -import { Socket } from "socket.io"; -import { RoomService } from "./services/room.service"; +import { Server, Socket } from "socket.io"; +import { RoomService } from "./room.service"; import { Logger, UsePipes, ValidationPipe } from "@nestjs/common"; import { EMIT_EVENT, LISTEN_EVENT } from "@/room/room.events"; -import { CreateRoomDto } from "@/room/dto/create-room.dto"; -import { WebsocketService } from "@/websocket/websocket.service"; -import { JoinRoomDto } from "@/room/dto/join-room.dto"; +import { + CreateRoomDto, + FinishRoomDto, + JoinRoomDto, + MoveIndexDto, + ReactionDto, + RoomIdDto, +} from "@/room/dto"; +import { InfraService } from "@/infra/infra.service"; import { RoomRepository } from "@/room/room.repository"; -import { ReactionDto } from "@/room/dto/reaction.dto"; -import { RoomLeaveService } from "@/room/services/room-leave.service"; -import { RoomCreateService } from "@/room/services/room-create.service"; -import { RoomJoinService } from "@/room/services/room-join.service"; -import { websocketConfig } from "@/websocket/websocket.config"; -import { FinishRoomDto } from "@/room/dto/finish-room.dto"; -import { RoomIdDto } from "@/room/dto/room-id.dto"; -import { MoveIndexDto } from "@/room/dto/move-index.dto"; - -@WebSocketGateway(websocketConfig) -export class RoomGateway implements OnGatewayDisconnect { - private logger: Logger = new Logger("Room Gateway"); + +import { gatewayConfig } from "@/infra/infra.config"; + +import { FullRoomException, InProgressException } from "@/room/exceptions/join-room-exceptions"; +import { createAdapter } from "@socket.io/redis-adapter"; + +@WebSocketGateway(gatewayConfig) +export class RoomGateway implements OnGatewayDisconnect, OnGatewayInit, OnGatewayConnection { + @WebSocketServer() + private server: Server; + private logger: Logger = new Logger("Websocket"); public constructor( private readonly roomService: RoomService, - private readonly roomLeaveService: RoomLeaveService, - private readonly roomCreateService: RoomCreateService, - private readonly roomJoinService: RoomJoinService, - private readonly socketService: WebsocketService, + private readonly infraService: InfraService, private readonly roomRepository: RoomRepository ) {} + public afterInit() { + const pubClient = this.infraService.getRedisClient(); + const subClient = pubClient.duplicate(); + const redisAdapter = createAdapter(pubClient, subClient); + this.server.adapter(redisAdapter); + this.infraService.setServer(this.server); + } + public async handleDisconnect(client: Socket) { await this.handleLeaveRoom(client); + this.logger.log(`Client disconnected: ${client.id}`); + await this.infraService.removeSocketMetadata(client); + } + + public async handleConnection(client: Socket) { + this.logger.log(`Client disconnected: ${client.id}`); + await this.infraService.createSocketMetadata(client); } @SubscribeMessage(LISTEN_EVENT.CREATE) @@ -46,8 +66,8 @@ export class RoomGateway implements OnGatewayDisconnect { @ConnectedSocket() client: Socket, @MessageBody() dto: CreateRoomDto ) { - // TODO: try - catch 로 에러 핸들링을 통해 이벤트 Emit 을 여기서 하기 - await this.roomCreateService.createRoom({ ...dto, socketId: client.id }); + const createRoomResponseDto = await this.roomService.createRoom(dto, client); + client.emit(EMIT_EVENT.CREATE, createRoomResponseDto); } @SubscribeMessage(LISTEN_EVENT.JOIN) @@ -56,19 +76,26 @@ export class RoomGateway implements OnGatewayDisconnect { @ConnectedSocket() client: Socket, @MessageBody() dto: JoinRoomDto ) { - await this.roomJoinService.joinRoom({ ...dto, socketId: client.id }); + try { + const joinRoomResponseDto = await this.roomService.joinRoom(dto, client); + client.emit(EMIT_EVENT.JOIN, joinRoomResponseDto); + } catch (e) { + if (e instanceof InProgressException) client.emit(EMIT_EVENT.IN_PROGRESS, {}); + else if (e instanceof FullRoomException) client.emit(EMIT_EVENT.FULL, {}); + else throw e; + } } @SubscribeMessage(LISTEN_EVENT.LEAVE) public async handleLeaveRoom(client: Socket) { - await this.roomLeaveService.leaveRoom(client); + await this.roomService.leaveRoom(client); } @SubscribeMessage(LISTEN_EVENT.FINISH) @UsePipes(new ValidationPipe({ transform: true })) public async handleFinishRoom(@MessageBody() dto: FinishRoomDto) { const roomId = await this.roomService.finishRoom(dto.roomId); - this.socketService.emitToRoom(roomId, EMIT_EVENT.FINISH); + this.infraService.emitToRoom(roomId, EMIT_EVENT.FINISH); } @SubscribeMessage(LISTEN_EVENT.REACTION) @@ -81,7 +108,7 @@ export class RoomGateway implements OnGatewayDisconnect { if (!room) return; - this.socketService.emitToRoom(room.id, EMIT_EVENT.REACTION, { + this.infraService.emitToRoom(room.id, EMIT_EVENT.REACTION, { socketId: client.id, reactionType: dto.reactionType, }); @@ -108,17 +135,14 @@ export class RoomGateway implements OnGatewayDisconnect { @ConnectedSocket() client: Socket, @MessageBody() dto: RoomIdDto ) { - this.socketService.emitToRoom(dto.roomId, EMIT_EVENT.NEXT_QUESTION, { + this.infraService.emitToRoom(dto.roomId, EMIT_EVENT.NEXT_QUESTION, { currentIndex: await this.roomService.increaseIndex(dto.roomId, client.id), }); } @SubscribeMessage(LISTEN_EVENT.CURRENT_INDEX) - public async handleCurrentIndex( - @ConnectedSocket() client: Socket, - @MessageBody() dto: RoomIdDto - ) { - this.socketService.emitToRoom(dto.roomId, EMIT_EVENT.CURRENT_INDEX, { + public async handleCurrentIndex(@MessageBody() dto: RoomIdDto) { + this.infraService.emitToRoom(dto.roomId, EMIT_EVENT.CURRENT_INDEX, { currentIndex: await this.roomService.getIndex(dto.roomId), }); } @@ -128,7 +152,7 @@ export class RoomGateway implements OnGatewayDisconnect { @ConnectedSocket() client: Socket, @MessageBody() dto: MoveIndexDto ) { - this.socketService.emitToRoom(dto.roomId, EMIT_EVENT.MOVE_INDEX, { + this.infraService.emitToRoom(dto.roomId, EMIT_EVENT.MOVE_INDEX, { currentIndex: await this.roomService.setIndex(dto.roomId, client.id, dto.index), }); } @@ -141,12 +165,12 @@ export class RoomGateway implements OnGatewayDisconnect { ) { try { const status = await this.roomService.setProgress(roomId, socketId, toStatus); - this.socketService.emitToRoom(roomId, eventName, { + this.infraService.emitToRoom(roomId, eventName, { status: "success", inProgress: status, }); } catch { - this.socketService.emitToRoom(roomId, eventName, { + this.infraService.emitToRoom(roomId, eventName, { status: "error", }); } diff --git a/backend/src/room/room.module.ts b/backend/src/room/room.module.ts index 7de1507d..41fffdf5 100644 --- a/backend/src/room/room.module.ts +++ b/backend/src/room/room.module.ts @@ -1,28 +1,20 @@ import { Module } from "@nestjs/common"; -import { RoomService } from "./services/room.service"; +import { RoomService } from "./room.service"; import { RoomGateway } from "./room.gateway"; import { RoomRepository } from "./room.repository"; import { RoomController } from "./room.controller"; import { RedisOmModule } from "@moozeh/nestjs-redis-om"; import { RoomEntity } from "./room.entity"; -import { RoomLeaveService } from "@/room/services/room-leave.service"; -import { RoomHostService } from "@/room/services/room-host.service"; import { QuestionListRepository } from "@/question-list/repository/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"; +import { InfraModule } from "@/infra/infra.module"; import { QuestionRepository } from "@/question-list/repository/question.respository"; @Module({ - imports: [RedisOmModule.forFeature([RoomEntity]), WebsocketModule], + imports: [RedisOmModule.forFeature([RoomEntity]), InfraModule], providers: [ RoomService, RoomGateway, RoomRepository, - RoomCreateService, - RoomJoinService, - RoomLeaveService, - RoomHostService, QuestionListRepository, QuestionRepository, ], diff --git a/backend/src/room/room.repository.ts b/backend/src/room/room.repository.ts index 7e1fd6c3..3de8f2d2 100644 --- a/backend/src/room/room.repository.ts +++ b/backend/src/room/room.repository.ts @@ -2,7 +2,6 @@ import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@moozeh/nestjs-redis-om"; import { Repository } from "redis-om"; import { RoomEntity } from "@/room/room.entity"; -import { RoomDto } from "@/room/dto/room.dto"; @Injectable() export class RoomRepository { @@ -12,71 +11,18 @@ export class RoomRepository { ) {} // TODO : .from 메서드 구현 필요? - public async getAllRoom(): Promise { - const allRooms = await this.roomRepository.search().return.all(); - return allRooms.map((room: RoomEntity) => { - const connectionMap = JSON.parse(room.connectionMap || "{}"); - return { - connectionMap: JSON.parse(room.connectionMap || "{}"), - createdAt: room.createdAt, - host: JSON.parse(room.host), - maxParticipants: room.maxParticipants, - maxQuestionListLength: room.maxQuestionListLength, - questionListId: room.questionListId, - questionListTitle: room.questionListTitle, - currentIndex: room.currentIndex, - status: room.status, - title: room.title, - id: room.id, - category: room.category, - inProgress: room.inProgress, - participants: Object.keys(connectionMap).length, - }; - }); + public async getAllRoom(): Promise { + return this.roomRepository.search().return.all(); } - public async getRoom(id: string): Promise { + public async getRoom(id: string): Promise { const room = await this.roomRepository.search().where("id").eq(id).return.first(); - if (!room) return null; - - const connectionMap = JSON.parse(room.connectionMap || "{}"); - - return { - category: room.category, - inProgress: room.inProgress, - connectionMap, - createdAt: room.createdAt, - currentIndex: room.currentIndex, - maxQuestionListLength: room.maxQuestionListLength, - questionListId: room.questionListId, - questionListTitle: room.questionListTitle, - host: JSON.parse(room.host), - participants: Object.keys(connectionMap).length, - maxParticipants: room.maxParticipants, - status: room.status, - title: room.title, - id: room.id, - }; + return room; } - public async setRoom(dto: RoomDto): Promise { - const room = new RoomEntity(); - room.id = dto.id; - room.category = dto.category; - room.inProgress = dto.inProgress; - room.title = dto.title; - room.status = dto.status; - room.currentIndex = dto.currentIndex; - room.connectionMap = JSON.stringify(dto.connectionMap); - room.maxParticipants = dto.maxParticipants; - room.maxQuestionListLength = dto.maxQuestionListLength; - room.questionListId = dto.questionListId; - room.questionListTitle = dto.questionListTitle; - room.createdAt = Date.now(); - room.host = JSON.stringify(dto.host); - - await this.roomRepository.save(room.id, room); + public async setRoom(entity: RoomEntity): Promise { + await this.roomRepository.save(entity.id, entity); } public async removeRoom(id: string): Promise { diff --git a/backend/src/room/room.service.ts b/backend/src/room/room.service.ts new file mode 100644 index 00000000..106350b7 --- /dev/null +++ b/backend/src/room/room.service.ts @@ -0,0 +1,262 @@ +import { Injectable } from "@nestjs/common"; +import { RoomEntity } from "@/room/room.entity"; +import { RoomRepository } from "@/room/room.repository"; +import { RoomListResponseDto } from "@/room/dto/all-room.dto"; +import { Socket } from "socket.io"; +import { CreateRoomDto, CreateRoomResponseDto } from "@/room/dto/create-room.dto"; +import { JoinRoomDto, JoinRoomResponseDto } from "@/room/dto/join-room.dto"; +import { Transactional } from "typeorm-transactional"; +import { Room, RoomStatus } from "@/room/domain/room"; +import { EMIT_EVENT } from "@/room/room.events"; +import { createHash } from "node:crypto"; +import { QuestionRepository } from "@/question-list/repository/question.respository"; +import { FullRoomException, InProgressException } from "@/room/exceptions/join-room-exceptions"; +import { InfraService } from "@/infra/infra.service"; +import { QuestionListRepository } from "@/question-list/repository/question-list.repository"; + +@Injectable() +export class RoomService { + private static ROOM_ID_CREATE_KEY = "room_id"; + + public constructor( + private readonly roomRepository: RoomRepository, + private readonly infraService: InfraService, + private readonly questionListRepository: QuestionListRepository, + private readonly questionRepository: QuestionRepository + ) {} + + public async leaveRoom(socket: Socket) { + const rooms = await this.infraService.getSocketMetadata(socket); + for await (const roomId of rooms.joinedRooms) + await this.processRoomLeave(socket.id, roomId); + } + + @Transactional() + public async createRoom( + createRoomDto: CreateRoomDto, + socket: Socket + ): Promise { + const currentTime = Date.now(); + const questionData = await this.useQuestionList(createRoomDto.questionListId); + const roomObj = { + ...createRoomDto, + id: await this.generateRoomId(), + inProgress: false, + connectionMap: {}, + participants: 0, + createdAt: currentTime, + maxQuestionListLength: questionData.content.length, + questionListTitle: questionData.title, + currentIndex: 0, + host: { + socketId: socket.id, + nickname: createRoomDto.nickname, + createAt: currentTime, + }, + }; + + const room = new Room(roomObj).build(); + + await this.roomRepository.setRoom(room); + + socket.emit(EMIT_EVENT.CREATE, room); + + return { + nickname: createRoomDto.nickname, + participants: 0, + questionListContents: questionData.content, + socketId: socket.id, + ...roomObj, + }; + } + + public async joinRoom(joinRoomDto: JoinRoomDto, socket: Socket): Promise { + const { roomId, nickname } = joinRoomDto; + + const room = Room.fromEntity(await this.roomRepository.getRoom(roomId)); + + if (!socket) throw new Error("Invalid Socket"); + if (!room.entity) throw new Error("RoomEntity Not found"); + + await this.infraService.joinRoom(socket, room.entity.id); + + if (room.entity.inProgress) throw new InProgressException(); + if (this.isFullRoom(room)) throw new FullRoomException(); + + room.addConnection({ + socketId: socket.id, + createAt: Date.now(), + nickname, + }); + + await this.roomRepository.setRoom(room.build()); + + const questionListContents = await this.questionRepository.getContentsByQuestionListId( + room.entity.questionListId + ); + + const obj = room.toObject(); + delete obj.connectionMap[socket.id]; + return { + participants: room.getParticipants(), + ...obj, + questionListContents, + }; + } + + public async getPublicRoom(inProgress?: boolean): Promise { + const rooms = await this.roomRepository.getAllRoom(); + + const filterFunction = (room: RoomEntity) => + room.status === RoomStatus.PUBLIC && + (inProgress === undefined || room.inProgress === inProgress); + + return rooms + .filter(filterFunction) + .sort((a, b) => b.createdAt - a.createdAt) + .map((room) => Room.fromEntity(room)) + .map((room) => ({ + ...room.toObject(), + host: room.getHost(), + participants: room.getParticipants(), + connectionMap: undefined, + })); + } + + public async setProgress(roomId: string, socketId: string, status: boolean) { + const room = Room.fromEntity(await this.roomRepository.getRoom(roomId)); + + if (!room.entity) throw new Error("cannot set progress"); + if (room.getHost().socketId !== socketId) throw new Error("only host can set process"); + + room.entity.inProgress = status; + if (!room.entity.inProgress) room.entity.currentIndex = 0; + await this.roomRepository.setRoom(room.build()); + return status; + } + + public async setIndex(roomId: string, socketId: string, index: number) { + const room = Room.fromEntity(await this.roomRepository.getRoom(roomId)); + + // TODO : 리팩토링 할 필요가 있어보임. + if ( + !room.entity || + !room.entity.inProgress || + room.getHost().socketId !== socketId || + index < 0 || + index >= room.entity.maxQuestionListLength + ) + return -1; + + room.entity.currentIndex = index; + await this.roomRepository.setRoom(room.build()); + return index; + } + + public async getIndex(roomId: string) { + return (await this.roomRepository.getRoom(roomId)).currentIndex; + } + + public async increaseIndex(roomId: string, socketId: string) { + const room = Room.fromEntity(await this.roomRepository.getRoom(roomId)); + + if (!room.entity || room.getHost().socketId !== socketId || !room.entity.inProgress) + return -1; + + const index = await this.setIndex(roomId, socketId, (await this.getIndex(roomId)) + 1); + + if (index === -1) return room.entity.maxQuestionListLength - 1; + + return index; + } + + public async finishRoom(roomId: string) { + await this.roomRepository.removeRoom(roomId); + return roomId; + } + + private async processRoomLeave(socketId: string, roomId: string) { + const room = Room.fromEntity(await this.roomRepository.getRoom(roomId)); + if (!room.entity) return; + + room.removeConnection(socketId); + + if (!Object.keys(room.getConnection()).length) return this.deleteRoom(room.entity.id); + + await this.roomRepository.setRoom(room.build()); + + if (room.getHost().socketId === socketId) await this.handleHostChange(socketId, room); + + this.infraService.emitToRoom(roomId, EMIT_EVENT.QUIT, { socketId }); + } + + private async handleHostChange(socketId: string, room: Room) { + if (room.getHost().socketId !== socketId) return; + + const newHost = await this.delegateHost(room); + + // TODO : throw new Exception : host changed + // 에러를 던지는 방식이 아닌 다른 방식으로 해결해야함. + this.infraService.emitToRoom(room.entity.id, EMIT_EVENT.CHANGE_HOST, { + nickname: newHost.nickname, + socketId: newHost.socketId, + }); + } + + private async deleteRoom(roomId: string) { + await this.roomRepository.removeRoom(roomId); + this.infraService.emitToRoom(roomId, EMIT_EVENT.QUIT, { roomId }); + } + + private getNewHost(room: Room) { + return Object.values(room.getConnection()).sort((a, b) => a.createAt - b.createAt)[0]; + } + + private async delegateHost(room: Room) { + const newHost = this.getNewHost(room); + + const found = room.getConnection()[newHost.socketId]; + if (!found) throw new Error("invalid new host id"); + + room.setHost(newHost); + + await this.roomRepository.setRoom(room.build()); + return newHost; + } + + private isFullRoom(room: Room): boolean { + return room.entity.maxParticipants <= Object.keys(room.getConnection()).length; + } + + @Transactional() + private async useQuestionList(questionListId: number) { + const questionData = await this.questionListRepository.findOne({ + where: { id: questionListId }, + }); + const questions = await this.questionRepository.getContentsByQuestionListId(questionListId); + questionData.usage += 1; + await this.questionListRepository.save(questionData); + return { + title: questionData.title, + content: questions, + }; + } + + // TODO: 동시성 고려해봐야하지 않을까? + private async generateRoomId() { + const client = this.infraService.getRedisClient(); + + const idString = await client.get(RoomService.ROOM_ID_CREATE_KEY); + + let id: number; + if (idString && !isNaN(parseInt(idString))) { + id = await client.incr(RoomService.ROOM_ID_CREATE_KEY); + } else { + id = parseInt(await client.set(RoomService.ROOM_ID_CREATE_KEY, "1")); + } + + return createHash("sha256") + .update(id + process.env.SESSION_HASH) + .digest("hex"); + } +} diff --git a/backend/src/room/services/room-create.service.ts b/backend/src/room/services/room-create.service.ts deleted file mode 100644 index e6247419..00000000 --- a/backend/src/room/services/room-create.service.ts +++ /dev/null @@ -1,78 +0,0 @@ -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/repository/question-list.repository"; -import { createHash } from "node:crypto"; -import "dotenv/config"; -import { Transactional } from "typeorm-transactional"; -import { QuestionRepository } from "@/question-list/repository/question.respository"; - -@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 questionRepository: QuestionRepository - ) {} - - @Transactional() - public async createRoom(dto: CreateRoomInternalDto) { - const { socketId, nickname } = dto; - const id = await this.generateRoomId(); - const socket = this.socketService.getSocket(socketId); - const currentTime = Date.now(); - const questionList = await this.questionListRepository.findOne({ - where: { id: dto.questionListId }, - }); - const questionListContent = await this.questionRepository.getContentsByQuestionListId( - dto.questionListId - ); - questionList.usage += 1; - await this.questionListRepository.save(questionList); - - const roomDto = { - ...dto, - id, - inProgress: false, - connectionMap: {}, - participants: 0, - questionListContents: questionListContent, - createdAt: currentTime, - maxQuestionListLength: questionListContent.length, - questionListTitle: questionList.title, - currentIndex: 0, - host: { - socketId: dto.socketId, - nickname, - createAt: currentTime, - }, - }; - - await this.roomRepository.setRoom(roomDto); - - socket.emit(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"); - } -} diff --git a/backend/src/room/services/room-host.service.ts b/backend/src/room/services/room-host.service.ts deleted file mode 100644 index 6e6efa14..00000000 --- a/backend/src/room/services/room-host.service.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { RoomDto } from "@/room/dto/room.dto"; -import { RoomRepository } from "@/room/room.repository"; -import { Injectable } from "@nestjs/common"; - -@Injectable() -export class RoomHostService { - public constructor(private readonly roomRepository: RoomRepository) {} - - private getNewHost(room: RoomDto) { - return Object.values(room.connectionMap).sort((a, b) => a.createAt - b.createAt)[0]; - } - - public async delegateHost(roomId: string) { - const room = await this.roomRepository.getRoom(roomId); - if (!room) throw new Error("Invalid room Id"); - - const newHost = this.getNewHost(room); - - const found = room.connectionMap[newHost.socketId]; - if (!found) throw new Error("invalid new host id"); - - room.host = newHost; - - await this.roomRepository.setRoom(room); - return newHost; - } -} diff --git a/backend/src/room/services/room-join.service.ts b/backend/src/room/services/room-join.service.ts deleted file mode 100644 index 397817dc..00000000 --- a/backend/src/room/services/room-join.service.ts +++ /dev/null @@ -1,63 +0,0 @@ -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"; -import { QuestionListRepository } from "@/question-list/repository/question-list.repository"; -import { QuestionRepository } from "@/question-list/repository/question.respository"; - -@Injectable() -export class RoomJoinService { - public constructor( - private readonly roomRepository: RoomRepository, - private readonly socketService: WebsocketService, - private readonly socketRepository: WebsocketRepository, - private readonly questionListRepository: QuestionListRepository, - private readonly questionRepository: QuestionRepository - ) {} - - public async joinRoom(dto: JoinRoomInternalDto) { - 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) throw new Error("RoomEntity Not found"); - - if (room.inProgress) return socket.emit(EMIT_EVENT.IN_PROGRESS, {}); - - if (this.isFullRoom(room)) return socket.emit(EMIT_EVENT.FULL, {}); - - socket.join(roomId); - - await this.socketRepository.joinRoom(socket.id, roomId); - - room.connectionMap[socketId] = { - socketId, - createAt: Date.now(), - nickname, - }; - - await this.roomRepository.setRoom(room); - - room.connectionMap[socketId] = undefined; - - const questionListContents = await this.questionRepository.getContentsByQuestionListId( - room.questionListId - ); - - // TODO: 성공 / 실패 여부를 전송하는데 있어서 결과에 따라 다르게 해야하는데.. 어떻게 관심 분리를 할까? - socket.emit(EMIT_EVENT.JOIN, { - ...room, - questionListContents, - }); - } - - private isFullRoom(room: RoomDto): boolean { - return room.maxParticipants <= Object.keys(room.connectionMap).length; - } -} diff --git a/backend/src/room/services/room-leave.service.ts b/backend/src/room/services/room-leave.service.ts deleted file mode 100644 index 893aebc9..00000000 --- a/backend/src/room/services/room-leave.service.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Injectable } from "@nestjs/common"; -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/room.events"; -import { Socket } from "socket.io"; -import { WebsocketRepository } from "@/websocket/websocket.repository"; - -@Injectable() -export class RoomLeaveService { - constructor( - private readonly socketService: WebsocketService, - private readonly socketRepository: WebsocketRepository, - private readonly roomRepository: RoomRepository, - private readonly roomHostService: RoomHostService - ) {} - - async leaveRoom(socket: Socket) { - const rooms = await this.socketRepository.getRoomOfSocket(socket.id); - - for await (const roomId of rooms.joinedRooms) - await this.processRoomLeave(socket.id, roomId); - } - - private async processRoomLeave(socketId: string, roomId: string) { - const room = await this.roomRepository.getRoom(roomId); - if (!room) return; - - delete room.connectionMap[socketId]; - - if (!Object.keys(room.connectionMap).length) return this.deleteRoom(room.id); - - await this.roomRepository.setRoom(room); - - if (room.host.socketId === socketId) await this.handleHostChange(socketId, room); - - this.socketService.emitToRoom(room.id, EMIT_EVENT.QUIT, { socketId }); - } - - private async handleHostChange(socketId: string, room: RoomDto) { - if (room.host.socketId !== socketId) return; - - const newHost = await this.roomHostService.delegateHost(room.id); - - // TODO : throw new Exception : host changed - this.socketService.emitToRoom(room.id, EMIT_EVENT.CHANGE_HOST, { - nickname: newHost.nickname, - socketId: newHost.socketId, - }); - } - - private async deleteRoom(roomId: string) { - await this.roomRepository.removeRoom(roomId); - this.socketService.emitToRoom(roomId, EMIT_EVENT.QUIT, { roomId }); - } -} diff --git a/backend/src/room/services/room.service.ts b/backend/src/room/services/room.service.ts index 74c2a600..e69de29b 100644 --- a/backend/src/room/services/room.service.ts +++ b/backend/src/room/services/room.service.ts @@ -1,83 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { RoomStatus } from "@/room/room.entity"; -import { RoomRepository } from "@/room/room.repository"; -import { RoomListResponseDto } from "@/room/dto/room-list.dto"; - -@Injectable() -export class RoomService { - public constructor(private readonly roomRepository: RoomRepository) {} - - public async getPublicRoom(inProgress?: boolean): Promise { - const rooms = await this.roomRepository.getAllRoom(); - return rooms - .filter( - (room) => - room.status === RoomStatus.PUBLIC && - (inProgress === undefined || room.inProgress === inProgress) - ) - .sort((a, b) => b.createdAt - a.createdAt) - .map((room) => ({ - createdAt: room.createdAt, - host: room.host, - maxParticipants: room.maxParticipants, - status: room.status, - title: room.title, - id: room.id, - category: room.category, - inProgress: room.inProgress, - questionListTitle: room.questionListTitle, - participants: room.participants, - })); - } - - public async setProgress(roomId: string, socketId: string, status: boolean) { - const room = await this.roomRepository.getRoom(roomId); - - if (!room) throw new Error("cannot set progress"); - if (room.host.socketId !== socketId) throw new Error("only host can set process"); - - room.inProgress = status; - if (!room.inProgress) room.currentIndex = 0; - await this.roomRepository.setRoom(room); - return status; - } - - public async setIndex(roomId: string, socketId: string, index: number) { - const room = await this.roomRepository.getRoom(roomId); - - // TODO : 리팩토링 할 필요가 있어보임. - if ( - !room || - !room.inProgress || - room.host.socketId !== socketId || - index < 0 || - index >= room.maxQuestionListLength - ) - return -1; - - room.currentIndex = index; - await this.roomRepository.setRoom(room); - return index; - } - - public async getIndex(roomId: string) { - return (await this.roomRepository.getRoom(roomId)).currentIndex; - } - - public async increaseIndex(roomId: string, socketId: string) { - const room = await this.roomRepository.getRoom(roomId); - - if (!room || room.host.socketId !== socketId || !room.inProgress) return -1; - - const index = await this.setIndex(roomId, socketId, (await this.getIndex(roomId)) + 1); - - if (index === -1) return room.maxQuestionListLength - 1; - - return index; - } - - public async finishRoom(roomId: string) { - await this.roomRepository.removeRoom(roomId); - return roomId; - } -} diff --git a/backend/src/signaling-server/sig-server.gateway.ts b/backend/src/signaling-server/sig-server.gateway.ts index b2854de8..3cec1c5a 100644 --- a/backend/src/signaling-server/sig-server.gateway.ts +++ b/backend/src/signaling-server/sig-server.gateway.ts @@ -1,14 +1,14 @@ import { + MessageBody, + SubscribeMessage, WebSocketGateway, WebSocketServer, - SubscribeMessage, - MessageBody, } from "@nestjs/websockets"; import { Server } from "socket.io"; import { EMIT_EVENT, LISTEN_EVENT } from "@/signaling-server/sig-server.event"; -import { websocketConfig } from "@/websocket/websocket.config"; +import { gatewayConfig } from "@/infra/infra.config"; -@WebSocketGateway(websocketConfig) +@WebSocketGateway(gatewayConfig) export class SigServerGateway { @WebSocketServer() private server: Server; diff --git a/backend/src/websocket/websocket.config.ts b/backend/src/websocket/websocket.config.ts deleted file mode 100644 index 0b70f6f0..00000000 --- a/backend/src/websocket/websocket.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import "dotenv/config"; - -export const websocketConfig = { - cors: { - origin: process.env.DOMAIN || "*", - }, -}; diff --git a/backend/src/websocket/websocket.gateway.ts b/backend/src/websocket/websocket.gateway.ts deleted file mode 100644 index fc3cfd31..00000000 --- a/backend/src/websocket/websocket.gateway.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Logger } from "@nestjs/common"; -import { Server, Socket } from "socket.io"; -import { - OnGatewayConnection, - OnGatewayDisconnect, - OnGatewayInit, - WebSocketGateway, - WebSocketServer, -} from "@nestjs/websockets"; -import Redis from "ioredis"; -import { createAdapter } from "@socket.io/redis-adapter"; -import { WebsocketService } from "@/websocket/websocket.service"; -import { WebsocketRepository } from "@/websocket/websocket.repository"; -import { websocketConfig } from "@/websocket/websocket.config"; - -@WebSocketGateway(websocketConfig) -export class WebsocketGateway implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit { - @WebSocketServer() - private server: Server; - - private logger: Logger = new Logger("Websocket"); - - public constructor( - private readonly websocketService: WebsocketService, - private readonly websocketRepository: WebsocketRepository - ) {} - - public 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.websocketService.setRedisClient(pubClient); - this.websocketService.setServer(this.server); - } - - public async handleConnection(client: Socket) { - this.logger.log(`Client connected: ${client.id}`); - await this.websocketRepository.register(client); - } - - public async handleDisconnect(client: Socket) { - this.logger.log(`Client disconnected: ${client.id}`); - await this.websocketRepository.clean(client); - } -} diff --git a/backend/src/websocket/websocket.module.ts b/backend/src/websocket/websocket.module.ts deleted file mode 100644 index 68cab311..00000000 --- a/backend/src/websocket/websocket.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Global, Module } from "@nestjs/common"; -import { WebsocketService } from "@/websocket/websocket.service"; -import { WebsocketGateway } from "@/websocket/websocket.gateway"; -import { WebsocketRepository } from "@/websocket/websocket.repository"; -import { RedisOmModule } from "@moozeh/nestjs-redis-om"; -import { RoomEntity } from "@/room/room.entity"; -import { WebsocketEntity } from "@/websocket/websocket.entity"; - -@Global() -@Module({ - imports: [RedisOmModule.forFeature([RoomEntity, WebsocketEntity])], - providers: [WebsocketGateway, WebsocketService, WebsocketRepository], - exports: [WebsocketService, WebsocketRepository], -}) -export class WebsocketModule {} diff --git a/backend/src/websocket/websocket.service.ts b/backend/src/websocket/websocket.service.ts deleted file mode 100644 index f5207507..00000000 --- a/backend/src/websocket/websocket.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { Server } from "socket.io"; -import Redis from "ioredis"; - -@Injectable() -export class WebsocketService { - constructor() {} - - private server: Server; - private redisClient: Redis; - - public setRedisClient(client: Redis) { - this.redisClient = client; - } - - public setServer(server: Server) { - this.server = server; - } - - public getRedisClient() { - return this.redisClient; - } - - public getServer() { - return this.server; - } - - public getSocket(socketId: string) { - return this.server.sockets.sockets.get(socketId); - } - - public emitToRoom(roomId: string, event: string, data?: any) { - this.server.to(roomId).emit(event, data); - } -}