diff --git a/backend/src/room/socket/room-socket.events.ts b/backend/src/room/room.events.ts similarity index 100% rename from backend/src/room/socket/room-socket.events.ts rename to backend/src/room/room.events.ts diff --git a/backend/src/room/socket/room-socket.gateway.ts b/backend/src/room/room.gateway.ts similarity index 54% rename from backend/src/room/socket/room-socket.gateway.ts rename to backend/src/room/room.gateway.ts index 5c7490f9..40a9b528 100644 --- a/backend/src/room/socket/room-socket.gateway.ts +++ b/backend/src/room/room.gateway.ts @@ -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) @@ -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) @@ -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) diff --git a/backend/src/room/room.module.ts b/backend/src/room/room.module.ts index e58dd357..cd1a0574 100644 --- a/backend/src/room/room.module.ts +++ b/backend/src/room/room.module.ts @@ -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], diff --git a/backend/src/room/services/room-create.service.ts b/backend/src/room/services/room-create.service.ts new file mode 100644 index 00000000..74f2c8c7 --- /dev/null +++ b/backend/src/room/services/room-create.service.ts @@ -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"); + } +} diff --git a/backend/src/room/services/room-join.service.ts b/backend/src/room/services/room-join.service.ts new file mode 100644 index 00000000..f242b15f --- /dev/null +++ b/backend/src/room/services/room-join.service.ts @@ -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; + } +} diff --git a/backend/src/room/services/room-leave.service.ts b/backend/src/room/services/room-leave.service.ts index f24b68b3..aaafbd1c 100644 --- a/backend/src/room/services/room-leave.service.ts +++ b/backend/src/room/services/room-leave.service.ts @@ -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 ) {} @@ -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) { diff --git a/backend/src/room/services/room.service.ts b/backend/src/room/services/room.service.ts index 880cb817..012ea881 100644 --- a/backend/src/room/services/room.service.ts +++ b/backend/src/room/services/room.service.ts @@ -1,30 +1,10 @@ import { Injectable } from "@nestjs/common"; import { RoomStatus } from "@/room/room.entity"; -import { Server, Socket } from "socket.io"; -import { CreateRoomInternalDto } from "@/room/dto/create-room.dto"; -import { generateRoomId } from "@/utils/generateRoomId"; -import { EMIT_EVENT } from "@/room/socket/room-socket.events"; -import { RoomSocketService } from "@/room/socket/room-socket.service"; -import { RoomLeaveService } from "@/room/services/room-leave.service"; import { RoomRepository } from "@/room/room.repository"; -import { RoomDto } from "@/room/dto/room.dto"; -import { JoinRoomInternalDto } from "@/room/dto/join-room.dto"; -import { RoomSocketRepository } from "@/room/socket/room-socket.repository"; -import { QuestionListRepository } from "@/question-list/question-list.repository"; @Injectable() export class RoomService { - public constructor( - private readonly roomRepository: RoomRepository, - private readonly roomLeaveService: RoomLeaveService, - private readonly socketService: RoomSocketService, - private readonly socketRepository: RoomSocketRepository, - private readonly questionListRepository: QuestionListRepository - ) {} - - public setServer(server: Server) { - this.socketService.setServer(server); - } + public constructor(private readonly roomRepository: RoomRepository) {} public async getPublicRoom() { const rooms = await this.roomRepository.getAllRoom(); @@ -33,70 +13,6 @@ export class RoomService { .sort((a, b) => b.createdAt - a.createdAt); } - public async leaveRoom(socket: Socket) { - return this.roomLeaveService.leaveRoom(socket); - } - - public async createRoom(dto: CreateRoomInternalDto) { - const roomId = generateRoomId(); - const currentTime = Date.now(); - - const roomDto = { - ...dto, - roomId, - connectionList: [ - { - socketId: dto.socketId, - createAt: currentTime, - nickname: dto.nickname, - }, - ], - questionListContents: await this.questionListRepository.getContentsByQuestionListId( - dto.questionListId - ), - createdAt: currentTime, - host: dto.socketId, - }; - await this.roomRepository.setRoom(roomDto); - - const socket = this.socketService.getSocket(dto.socketId); - socket.join(roomId); - await this.socketRepository.joinRoom(socket.id, roomId); - this.socketService.emitToRoom(roomId, EMIT_EVENT.CREATE, roomDto); - } - - 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.roomId === null) throw new Error("Redis: RoomEntity Entity type error"); - - // TODO : 에러를 보내기 -> Exception 생성해서 분류 - if (this.isFullRoom(room)) return socket.emit(EMIT_EVENT.FULL, {}); - - // TODO : join room 하는 단계가 3가지가 같이 혼재 -> 이것만 따로 묶어보기? - socket.join(roomId); - await this.socketRepository.joinRoom(socket.id, roomId); - room.connectionList.push({ - socketId, - createAt: Date.now(), - nickname, - }); - - await this.roomRepository.setRoom(room); - - // TODO: 성공 / 실패 여부를 전송하는데 있어서 결과에 따라 다르게 해야하는데.. 어떻게 관심 분리를 할까? - this.socketService.emitToRoom(roomId, EMIT_EVENT.JOIN, room); - } - - private isFullRoom(room: RoomDto): boolean { - return room.maxParticipants <= room.connectionList.length; - } - 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 470fa0ea..3a3c909a 100644 --- a/backend/src/signaling-server/sig-server.gateway.ts +++ b/backend/src/signaling-server/sig-server.gateway.ts @@ -1,28 +1,17 @@ import { WebSocketGateway, WebSocketServer, - OnGatewayConnection, - OnGatewayDisconnect, SubscribeMessage, MessageBody, } from "@nestjs/websockets"; import { Server } from "socket.io"; -import { Logger } from "@nestjs/common"; import { EMIT_EVENT, LISTEN_EVENT } from "@/signaling-server/sig-server.event"; @WebSocketGateway() -export class SigServerGateway implements OnGatewayConnection, OnGatewayDisconnect { +export class SigServerGateway { @WebSocketServer() private server: Server; - handleConnection(socket: any) { - Logger.log(`Client connected in signaling server: ${socket.id}`); - } - - handleDisconnect(socket: any) { - Logger.log(`Client disconnected signaling server: ${socket.id}`); - } - @SubscribeMessage(LISTEN_EVENT.OFFER) handleOffer( @MessageBody() diff --git a/backend/src/utils/generateRoomId.ts b/backend/src/utils/generateRoomId.ts deleted file mode 100644 index c808594a..00000000 --- a/backend/src/utils/generateRoomId.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createHash } from "node:crypto"; -import "dotenv/config"; - -let id = parseInt(process.env.SESSION_HASH_START); -export function generateRoomId() { - return createHash("sha256") - .update(id++ + process.env.SESSION_HASH) - .digest("hex"); -} \ No newline at end of file diff --git a/backend/src/room/socket/room-socket.entity.ts b/backend/src/websocket/websocket.entity.ts similarity index 81% rename from backend/src/room/socket/room-socket.entity.ts rename to backend/src/websocket/websocket.entity.ts index 806f051e..a6f3b482 100644 --- a/backend/src/room/socket/room-socket.entity.ts +++ b/backend/src/websocket/websocket.entity.ts @@ -1,7 +1,7 @@ import { Entity, Field, Schema } from "nestjs-redis-om"; @Schema("socket") -export class RoomSocketEntity extends Entity { +export class WebsocketEntity extends Entity { @Field({ type: "string", indexed: true }) id: string; diff --git a/backend/src/websocket/websocket.gateway.ts b/backend/src/websocket/websocket.gateway.ts new file mode 100644 index 00000000..a9b88cd9 --- /dev/null +++ b/backend/src/websocket/websocket.gateway.ts @@ -0,0 +1,51 @@ +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"; + +@WebSocketGateway() +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 new file mode 100644 index 00000000..cf3842fd --- /dev/null +++ b/backend/src/websocket/websocket.module.ts @@ -0,0 +1,15 @@ +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 "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/room/socket/room-socket.repository.ts b/backend/src/websocket/websocket.repository.ts similarity index 81% rename from backend/src/room/socket/room-socket.repository.ts rename to backend/src/websocket/websocket.repository.ts index ba3af2bb..8bd27e59 100644 --- a/backend/src/room/socket/room-socket.repository.ts +++ b/backend/src/websocket/websocket.repository.ts @@ -2,17 +2,17 @@ import { Injectable } from "@nestjs/common"; import { Socket } from "socket.io"; import { InjectRepository } from "nestjs-redis-om"; import { Repository } from "redis-om"; -import { RoomSocketEntity } from "@/room/socket/room-socket.entity"; +import { WebsocketEntity } from "@/websocket/websocket.entity"; @Injectable() -export class RoomSocketRepository { +export class WebsocketRepository { public constructor( - @InjectRepository(RoomSocketEntity) - private readonly socketRepository: Repository + @InjectRepository(WebsocketEntity) + private readonly socketRepository: Repository ) {} public async register(socket: Socket) { - const entity = new RoomSocketEntity(); + const entity = new WebsocketEntity(); entity.id = socket.id; entity.joinedRooms = []; await this.socketRepository.remove(entity.id); diff --git a/backend/src/room/socket/room-socket.service.ts b/backend/src/websocket/websocket.service.ts similarity index 60% rename from backend/src/room/socket/room-socket.service.ts rename to backend/src/websocket/websocket.service.ts index 46b45a21..f5207507 100644 --- a/backend/src/room/socket/room-socket.service.ts +++ b/backend/src/websocket/websocket.service.ts @@ -1,25 +1,32 @@ import { Injectable } from "@nestjs/common"; import { Server } from "socket.io"; +import Redis from "ioredis"; @Injectable() -export class RoomSocketService { +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 getSocket(socketId: string) { - return this.server.sockets.sockets.get(socketId); + public getRedisClient() { + return this.redisClient; } - public async leaveRoom(socketId: string, roomId: string) { - const socket = this.server.sockets.sockets.get(socketId); - if (socket) { - await socket.leave(roomId); - } + public getServer() { + return this.server; + } + + public getSocket(socketId: string) { + return this.server.sockets.sockets.get(socketId); } public emitToRoom(roomId: string, event: string, data?: any) { diff --git a/docker-compose.yml b/docker-compose.yml index 9370a4b2..aae58ddb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,11 +16,11 @@ services: volumes: - ./mysql-data:/var/lib/mysql - redis: - image: redis:latest - container_name: redis + redis-stack: + image: redis/redis-stack:latest + container_name: redis-stack ports: - - '6379:6379' - command: redis-server --port 6379 + - "6379:6379" + - "8001:8001" volumes: - - ./redis-data:/data \ No newline at end of file + - ./redis-data/:/data \ No newline at end of file diff --git a/frontend/src/constraints/CategoryData.ts b/frontend/src/constants/CategoryData.ts similarity index 100% rename from frontend/src/constraints/CategoryData.ts rename to frontend/src/constants/CategoryData.ts diff --git a/frontend/src/constraints/LayoutConstant.ts b/frontend/src/constants/LayoutConstant.ts similarity index 100% rename from frontend/src/constraints/LayoutConstant.ts rename to frontend/src/constants/LayoutConstant.ts diff --git a/frontend/src/constants/WebSocket/SessionEvent.ts b/frontend/src/constants/WebSocket/SessionEvent.ts new file mode 100644 index 00000000..64deaae6 --- /dev/null +++ b/frontend/src/constants/WebSocket/SessionEvent.ts @@ -0,0 +1,17 @@ +export const SESSION_EMIT_EVENT = { + CREATE: "client:room__create", + JOIN: "client:room__join", + LEAVE: "client:room__leave", + FINISH: "client:room__finish", + REACTION: "client:room__reaction", +} as const; + +export const SESSION_LISTEN_EVENT = { + CREATE: "server:room__create", + QUIT: "server:room__quit", + FULL: "server:room__full", + JOIN: "server:room__join", + FINISH: "server:room__finish", + CHANGE_HOST: "server:room__change_host", + REACTION: "server:room__reaction", +} as const; diff --git a/frontend/src/constants/WebSocket/SignalingEvent.ts b/frontend/src/constants/WebSocket/SignalingEvent.ts new file mode 100644 index 00000000..f42624b2 --- /dev/null +++ b/frontend/src/constants/WebSocket/SignalingEvent.ts @@ -0,0 +1,11 @@ +export const SIGNAL_EMIT_EVENT = { + OFFER: "client:sig_server__offer", + ANSWER: "client:sig_server__answer", + CANDIDATE: "client:sig_server__candidate", +}; + +export const SIGNAL_LISTEN_EVENT = { + OFFER: "server:sig_server__offer", + ANSWER: "server:sig_server__answer", + CANDIDATE: "server:sig_server__candidate", +}; diff --git a/frontend/src/hooks/session/usePeerConnection.ts b/frontend/src/hooks/session/usePeerConnection.ts index eeef5d95..9b946177 100644 --- a/frontend/src/hooks/session/usePeerConnection.ts +++ b/frontend/src/hooks/session/usePeerConnection.ts @@ -1,6 +1,7 @@ import { useRef, useState } from "react"; import { Socket } from "socket.io-client"; import { PeerConnection } from "../type/session"; +import { SIGNAL_EMIT_EVENT } from "@/constants/WebSocket/SignalingEvent.ts"; interface User { id?: string; @@ -48,7 +49,7 @@ const usePeerConnection = (socket: Socket) => { // 가능한 연결 경로를 찾을 때마다 상대에게 알려줌 pc.onicecandidate = (e: RTCPeerConnectionIceEvent) => { if (e.candidate && socket) { - socket.emit("candidate", { + socket.emit(SIGNAL_EMIT_EVENT.CANDIDATE, { candidateReceiveID: peerSocketId, candidate: e.candidate, candidateSendID: socket.id, @@ -100,7 +101,7 @@ const usePeerConnection = (socket: Socket) => { .then((offer) => pc.setLocalDescription(offer)) .then(() => { if (socket && pc.localDescription) { - socket.emit("offer", { + socket.emit(SIGNAL_EMIT_EVENT.OFFER, { offerReceiveID: peerSocketId, sdp: pc.localDescription, offerSendID: socket.id, diff --git a/frontend/src/hooks/session/useReaction.ts b/frontend/src/hooks/session/useReaction.ts index 64c653ff..9324d81e 100644 --- a/frontend/src/hooks/session/useReaction.ts +++ b/frontend/src/hooks/session/useReaction.ts @@ -1,6 +1,7 @@ import { Dispatch, SetStateAction, useCallback, useRef } from "react"; import { Socket } from "socket.io-client"; import { PeerConnection } from "../type/session"; +import { SESSION_EMIT_EVENT } from "@/constants/WebSocket/SessionEvent.ts"; const REACTION_DURATION = 3000; @@ -17,7 +18,7 @@ export const useReaction = ( const emitReaction = useCallback( (reactionType: string) => { if (socket) { - socket.emit("reaction", { + socket.emit(SESSION_EMIT_EVENT.REACTION, { roomId: sessionId, reaction: reactionType, }); diff --git a/frontend/src/hooks/session/useSession.ts b/frontend/src/hooks/session/useSession.ts index 7cb5ce02..fb310443 100644 --- a/frontend/src/hooks/session/useSession.ts +++ b/frontend/src/hooks/session/useSession.ts @@ -10,6 +10,7 @@ import { usePeerConnectionCleanup } from "@hooks/session/usePeerConnectionCleanu import { useReaction } from "@hooks/session/useReaction"; import { useSocketEvents } from "./useSocketEvents"; import { Socket } from "socket.io-client"; +import { SESSION_EMIT_EVENT } from "@/constants/WebSocket/SessionEvent.ts"; export const useSession = (sessionId: string) => { const { socket } = useSocket(); @@ -104,7 +105,7 @@ export const useSession = (sessionId: string) => { return; } - socket.emit("join_room", { roomId: sessionId, nickname }); + socket.emit(SESSION_EMIT_EVENT.JOIN, { roomId: sessionId, nickname }); }; const participants: Participant[] = useMemo( diff --git a/frontend/src/hooks/session/useSocketEvents.ts b/frontend/src/hooks/session/useSocketEvents.ts index 7cc0d4f4..abdf6a56 100644 --- a/frontend/src/hooks/session/useSocketEvents.ts +++ b/frontend/src/hooks/session/useSocketEvents.ts @@ -15,6 +15,11 @@ import { RoomMetadata, PeerConnection, } from "@hooks/type/session"; +import { + SIGNAL_EMIT_EVENT, + SIGNAL_LISTEN_EVENT, +} from "@/constants/WebSocket/SignalingEvent.ts"; +import { SESSION_LISTEN_EVENT } from "@/constants/WebSocket/SessionEvent.ts"; interface UseSocketEventsProps { socket: Socket | null; @@ -128,7 +133,7 @@ export const useSocketEvents = ({ const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); - socket.emit("answer", { + socket.emit(SIGNAL_EMIT_EVENT.ANSWER, { answerReceiveID: data.offerSendID, sdp: answer, answerSendID: socket.id, @@ -164,29 +169,29 @@ export const useSocketEvents = ({ } }; - socket.on("all_users", handleAllUsers); - socket.on("getOffer", handleGetOffer); - socket.on("getAnswer", handleGetAnswer); - socket.on("getCandidate", handleGetCandidate); - socket.on("user_exit", handleUserExit); - socket.on("room_full", () => { + socket.on(SIGNAL_LISTEN_EVENT.OFFER, handleGetOffer); + socket.on(SIGNAL_LISTEN_EVENT.ANSWER, handleGetAnswer); + socket.on(SIGNAL_LISTEN_EVENT.CANDIDATE, handleGetCandidate); + socket.on(SESSION_LISTEN_EVENT.JOIN, handleAllUsers); + socket.on(SESSION_LISTEN_EVENT.QUIT, handleUserExit); + socket.on(SESSION_LISTEN_EVENT.FULL, () => { toast.error("해당 세션은 이미 유저가 가득 찼습니다."); navigate("/sessions"); }); - socket.on("master_changed", handleHostChange); - socket.on("reaction", handleReaction); - socket.on("room_finished", handleRoomFinished); + socket.on(SESSION_LISTEN_EVENT.CHANGE_HOST, handleHostChange); + socket.on(SESSION_LISTEN_EVENT.REACTION, handleReaction); + socket.on(SESSION_LISTEN_EVENT.FINISH, handleRoomFinished); return () => { - socket.off("all_users", handleAllUsers); - socket.off("getOffer", handleGetOffer); - socket.off("getAnswer", handleGetAnswer); - socket.off("getCandidate", handleGetCandidate); - socket.off("user_exit"); - socket.off("room_full"); - socket.off("master_changed", handleHostChange); - socket.off("room_finished", handleRoomFinished); - socket.off("reaction", handleReaction); + socket.off(SIGNAL_LISTEN_EVENT.OFFER, handleGetOffer); + socket.off(SIGNAL_LISTEN_EVENT.ANSWER, handleGetAnswer); + socket.off(SIGNAL_LISTEN_EVENT.CANDIDATE, handleGetCandidate); + socket.off(SESSION_LISTEN_EVENT.JOIN, handleAllUsers); + socket.off(SESSION_LISTEN_EVENT.QUIT); + socket.off(SESSION_LISTEN_EVENT.FULL); + socket.off(SESSION_LISTEN_EVENT.CHANGE_HOST, handleHostChange); + socket.off(SESSION_LISTEN_EVENT.REACTION, handleReaction); + socket.off(SESSION_LISTEN_EVENT.FINISH, handleRoomFinished); if (reactionTimeouts.current) { Object.values(reactionTimeouts.current).forEach(clearTimeout); diff --git a/frontend/src/pages/CreateSessionPage.tsx b/frontend/src/pages/CreateSessionPage.tsx index 35e3cc50..73cb8b56 100644 --- a/frontend/src/pages/CreateSessionPage.tsx +++ b/frontend/src/pages/CreateSessionPage.tsx @@ -4,7 +4,7 @@ import { useNavigate } from "react-router-dom"; import { useEffect } from "react"; import useSessionFormStore from "@/stores/useSessionFormStore"; import Sidebar from "@components/common/Sidebar.tsx"; -import { sectionWithSidebar } from "@/constraints/LayoutConstant.ts"; +import { sectionWithSidebar } from "@/constants/LayoutConstant.ts"; const CreateSessionPage = () => { const navigate = useNavigate(); diff --git a/frontend/src/pages/MyPage.tsx b/frontend/src/pages/MyPage.tsx index 35aa6125..aa6df06f 100644 --- a/frontend/src/pages/MyPage.tsx +++ b/frontend/src/pages/MyPage.tsx @@ -1,4 +1,4 @@ -import { sectionWithSidebar } from "@/constraints/LayoutConstant.ts"; +import { sectionWithSidebar } from "@/constants/LayoutConstant.ts"; import { useEffect, useState } from "react"; import useAuth from "@hooks/useAuth.ts"; import { useNavigate } from "react-router-dom"; diff --git a/frontend/src/pages/QuestionDetailPage.tsx b/frontend/src/pages/QuestionDetailPage.tsx index 45d829d1..90f0d676 100644 --- a/frontend/src/pages/QuestionDetailPage.tsx +++ b/frontend/src/pages/QuestionDetailPage.tsx @@ -1,6 +1,6 @@ import { useNavigate, useParams } from "react-router-dom"; import Sidebar from "@/components/common/Sidebar"; -import { sectionWithSidebar } from "@/constraints/LayoutConstant"; +import { sectionWithSidebar } from "@/constants/LayoutConstant.ts"; import QuestionTitle from "@/components/questions/detail/QuestionTitle.tsx"; import QuestionList from "@/components/questions/detail/QuestionList.tsx"; import { useGetQuestion } from "@hooks/api/useGetQuestion.ts"; diff --git a/frontend/src/pages/QuestionListPage.tsx b/frontend/src/pages/QuestionListPage.tsx index 0522de31..1825b9c0 100644 --- a/frontend/src/pages/QuestionListPage.tsx +++ b/frontend/src/pages/QuestionListPage.tsx @@ -10,8 +10,7 @@ import { useNavigate, useSearchParams } from "react-router-dom"; import axios from "axios"; import useAuth from "@hooks/useAuth.ts"; import CreateButton from "@components/common/CreateButton.tsx"; -import { options } from "@/constraints/CategoryData.ts"; - +import { options } from "@/constants/CategoryData.ts"; interface QuestionList { id: number; diff --git a/frontend/src/pages/SessionListPage.tsx b/frontend/src/pages/SessionListPage.tsx index a335d6fd..00296cdb 100644 --- a/frontend/src/pages/SessionListPage.tsx +++ b/frontend/src/pages/SessionListPage.tsx @@ -7,7 +7,7 @@ import Select from "@components/common/Select.tsx"; import SessionList from "@components/sessions/list/SessionList.tsx"; import axios from "axios"; import CreateButton from "@components/common/CreateButton.tsx"; -import { options } from "@/constraints/CategoryData.ts"; +import { options } from "@/constants/CategoryData.ts"; interface Session { id: number; @@ -63,7 +63,11 @@ const SessionListPage = () => {

스터디 세션 목록

- navigate("/sessions/create")} text={"새로운 세션"} diff --git a/frontend/src/pages/SessionPage.tsx b/frontend/src/pages/SessionPage.tsx index dbec0150..a5e9ec34 100644 --- a/frontend/src/pages/SessionPage.tsx +++ b/frontend/src/pages/SessionPage.tsx @@ -5,10 +5,19 @@ import SessionToolbar from "@components/session/SessionToolbar.tsx"; import { useSession } from "@hooks/session/useSession"; import useSocket from "@hooks/useSocket"; import SessionHeader from "@components/session/SessionHeader"; +import { useEffect } from "react"; +import useToast from "@hooks/useToast.ts"; const SessionPage = () => { const { sessionId } = useParams(); - if (!sessionId) return null; + const toast = useToast(); + + useEffect(() => { + if (!sessionId) { + toast.error("유효하지 않은 세션 아이디입니다."); + } + }, []); + const { socket } = useSocket(); const { nickname, @@ -29,7 +38,7 @@ const SessionPage = () => { setSelectedVideoDeviceId, joinRoom, emitReaction, - } = useSession(sessionId); + } = useSession(sessionId!); return (
diff --git a/frontend/src/stores/useSocketStore.ts b/frontend/src/stores/useSocketStore.ts index e7f46a3f..b01789d3 100644 --- a/frontend/src/stores/useSocketStore.ts +++ b/frontend/src/stores/useSocketStore.ts @@ -11,7 +11,7 @@ const useSocketStore = create((set) => ({ socket: null, connect: (socketURL) => { const newSocket = io(socketURL); - + console.log(newSocket); newSocket.on("connect", socketConnectHandler); newSocket.on("connect_error", socketErrorHandler); set({ socket: newSocket });