diff --git a/be/gameServer/src/modules/rooms/rooms.gateway.spec.ts b/be/gameServer/src/modules/rooms/rooms.gateway.spec.ts index b293073..dbbc628 100644 --- a/be/gameServer/src/modules/rooms/rooms.gateway.spec.ts +++ b/be/gameServer/src/modules/rooms/rooms.gateway.spec.ts @@ -34,6 +34,7 @@ describe('RoomsGateway', () => { mockServer = { to: jest.fn().mockReturnValue({ emit: jest.fn(), + fetchSockets: jest.fn(), }), } as unknown as Server; @@ -395,7 +396,7 @@ describe('RoomsGateway', () => { mockClient.data = { roomId, playerNickname }; - await gateway.handleSetReady(playerNickname, mockClient); + await gateway.handleSetReady(mockClient); expect(redisService.set).toHaveBeenCalledWith( `room:${roomId}`, @@ -433,7 +434,7 @@ describe('RoomsGateway', () => { mockClient.data = { roomId, playerNickname }; - await gateway.handleSetReady(playerNickname, mockClient); + await gateway.handleSetReady(mockClient); expect(mockClient.emit).toHaveBeenCalledWith( 'error', @@ -442,97 +443,61 @@ describe('RoomsGateway', () => { }); }); - // describe('handleKickPlayer', () => { - // it('호스트만 플레이어를 강퇴할 수 있어야 한다.', async () => { - // const roomId = 'testRoomId'; - // const playerNickname = 'Player1'; - // const roomData: RoomDataDto = { - // roomId, - // roomName: 'testRoom', - // hostNickname: 'host', - // players: [ - // { playerNickname: 'host', isReady: false }, - // { playerNickname: 'Player1', isReady: false }, - // ], - // status: 'waiting', - // }; - - // jest - // .spyOn(redisService, 'get') - // .mockResolvedValueOnce(JSON.stringify(roomData)); - - // mockClient.data = { roomId, playerNickname }; - - // await gateway.handleKickPlayer('Player1', mockClient); - - // expect(mockClient.emit).toHaveBeenCalledWith( - // 'error', - // 'Only host can kick players', - // ); - // }); - - // it('플레이어가 방에 없으면 에러를 전송해야 한다.', async () => { - // const roomId = 'testRoomId'; - // const playerNickname = 'host'; - // const roomData: RoomDataDto = { - // roomId, - // roomName: 'testRoom', - // hostNickname: 'host', - // players: [ - // { playerNickname: 'host', isReady: false }, - // { playerNickname: 'Player1', isReady: false }, - // ], - // status: 'waiting', - // }; - - // jest - // .spyOn(redisService, 'get') - // .mockResolvedValueOnce(JSON.stringify(roomData)); - - // mockClient.data = { roomId, playerNickname }; - - // await gateway.handleKickPlayer('Player2', mockClient); - - // expect(mockClient.emit).toHaveBeenCalledWith( - // 'error', - // 'Player not found in room', - // ); - // }); - - // // it('호스트가 플레이어를 강퇴할 수 있어야 한다.', async () => { - // // const roomId = 'testRoomId'; - // // const nickname = 'host'; - // // const playerNickname = 'Player1'; - // // const roomData: RoomDataDto = { - // // roomId, - // // roomName: 'testRoom', - // // hostNickname: 'host', - // // players: [ - // // { playerNickname: 'host', isReady: false }, - // // { playerNickname: 'Player1', isReady: false }, - // // ], - // // status: 'waiting', - // // }; - - // // jest - // // .spyOn(redisService, 'get') - // // .mockResolvedValueOnce(JSON.stringify(roomData)); - - // // mockClient.data = { roomId, playerNickname: nickname }; - - // // await gateway.handleKickPlayer(playerNickname, mockClient); - - // // expect(redisService.set).toHaveBeenCalledWith( - // // `room:${roomId}`, - // // JSON.stringify({ - // // ...roomData, - // // players: [{ playerNickname: 'host', isReady: false }], - // // }), - // // 'roomUpdate', - // // ); - // // expect(mockServer.to(roomId).emit).toHaveBeenCalledWith('updateUsers', [ - // // { playerNickname: 'host', isReady: false }, - // // ]); - // // }); - // }); + describe('handleKickPlayer', () => { + it('호스트만 플레이어를 강퇴할 수 있어야 한다.', async () => { + const roomId = 'testRoomId'; + const playerNickname = 'Player1'; + const roomData: RoomDataDto = { + roomId, + roomName: 'testRoom', + hostNickname: 'host', + players: [ + { playerNickname: 'host', isReady: false }, + { playerNickname: 'Player1', isReady: false }, + ], + status: 'waiting', + }; + + jest + .spyOn(redisService, 'get') + .mockResolvedValueOnce(JSON.stringify(roomData)); + + mockClient.data = { roomId, playerNickname }; + + await gateway.handleKickPlayer('Player1', mockClient); + + expect(mockClient.emit).toHaveBeenCalledWith( + 'error', + 'Only host can kick players', + ); + }); + + it('플레이어가 방에 없으면 에러를 전송해야 한다.', async () => { + const roomId = 'testRoomId'; + const playerNickname = 'host'; + const roomData: RoomDataDto = { + roomId, + roomName: 'testRoom', + hostNickname: 'host', + players: [ + { playerNickname: 'host', isReady: false }, + { playerNickname: 'Player1', isReady: false }, + ], + status: 'waiting', + }; + + jest + .spyOn(redisService, 'get') + .mockResolvedValueOnce(JSON.stringify(roomData)); + + mockClient.data = { roomId, playerNickname }; + + await gateway.handleKickPlayer('Player2', mockClient); + + expect(mockClient.emit).toHaveBeenCalledWith( + 'error', + 'Player not found in room', + ); + }); + }); }); diff --git a/be/gameServer/src/modules/rooms/rooms.gateway.ts b/be/gameServer/src/modules/rooms/rooms.gateway.ts index c905fec..48e236e 100644 --- a/be/gameServer/src/modules/rooms/rooms.gateway.ts +++ b/be/gameServer/src/modules/rooms/rooms.gateway.ts @@ -185,12 +185,9 @@ export class RoomsGateway implements OnGatewayDisconnect { } @SubscribeMessage('setReady') - async handleSetReady( - @MessageBody() playerNickname: string, - @ConnectedSocket() client: Socket, - ) { + async handleSetReady(@ConnectedSocket() client: Socket) { try { - const { roomId } = client.data; + const { roomId, playerNickname } = client.data; const roomDataString = await this.redisService.get( `room:${roomId}`, ); @@ -201,7 +198,7 @@ export class RoomsGateway implements OnGatewayDisconnect { ); if (player) { - player.isReady = true; + player.isReady = !player.isReady; await this.redisService.set(`room:${roomId}`, JSON.stringify(roomData)); this.server.to(roomId).emit('updateUsers', roomData.players); } else { @@ -213,65 +210,57 @@ export class RoomsGateway implements OnGatewayDisconnect { } } - // @SubscribeMessage('kickPlayer') - // async handleKickPlayer( - // @MessageBody() playerNickname: string, - // @ConnectedSocket() client: Socket, - // ) { - // try { - // const { roomId } = client.data; - // const roomDataString = await this.redisService.get( - // `room:${roomId}`, - // ); - // const roomData: RoomDataDto = JSON.parse(roomDataString); - - // if (roomData.hostNickname !== client.data.playerNickname) { - // client.emit('error', 'Only host can kick players'); - // return; - // } - - // const playerIndex = roomData.players.findIndex( - // (p) => p.playerNickname === playerNickname, - // ); - - // if (playerIndex === -1) { - // client.emit('error', 'Player not found in room'); - // return; - // } - - // const roomSockets = this.server.sockets.adapter.rooms.get(roomId); - // console.log('Server Sockets Adapter:', this.server.sockets.adapter); - // if (!roomSockets) { - // client.emit('error', 'Room not found'); - // return; - // } - - // let targetSocketId: string | undefined; - // // `roomSockets`는 `Set` 형태이므로 직접 순회하여 찾음 - // for (const socketId of roomSockets) { - // const socket = this.server.sockets.sockets.get(socketId); - // if (socket?.data.playerNickname === playerNickname) { - // targetSocketId = socketId; - // break; - // } - // } - - // roomData.players.splice(playerIndex, 1); - // await this.redisService.set( - // `room:${roomId}`, - // JSON.stringify(roomData), - // 'roomUpdate', - // ); - - // const targetClient = this.server.sockets.sockets.get(targetSocketId); - // targetClient?.disconnect(); - - // this.server.to(roomId).emit('updateUsers', roomData.players); - - // this.logger.log(`Player ${playerNickname} kicked from room ${roomId}`); - // } catch (error) { - // this.logger.error(`Error kicking player: ${error.message}`); - // client.emit('error', 'Failed to kick player'); - // } - // } + @SubscribeMessage('kickPlayer') + async handleKickPlayer( + @MessageBody() playerNickname: string, + @ConnectedSocket() client: Socket, + ) { + try { + const { roomId } = client.data; + const roomDataString = await this.redisService.get( + `room:${roomId}`, + ); + const roomData: RoomDataDto = JSON.parse(roomDataString); + + if (roomData.hostNickname !== client.data.playerNickname) { + client.emit('error', 'Only host can kick players'); + return; + } + + const playerIndex = roomData.players.findIndex( + (p) => p.playerNickname === playerNickname, + ); + + if (playerIndex === -1) { + client.emit('error', 'Player not found in room'); + return; + } + + const socketsInRoom = await this.server.in(roomId).fetchSockets(); + const targetSocket = socketsInRoom.find( + (socket) => socket.data.playerNickname === playerNickname, + ); + + if (targetSocket) { + targetSocket.disconnect(true); + this.logger.log( + `Player ${playerNickname} disconnected from room ${roomId}`, + ); + } + + roomData.players.splice(playerIndex, 1); + await this.redisService.set( + `room:${roomId}`, + JSON.stringify(roomData), + 'roomUpdate', + ); + + this.server.to(roomId).emit('kicked', playerNickname); + + this.logger.log(`Player ${playerNickname} kicked from room ${roomId}`); + } catch (error) { + this.logger.error(`Error kicking player: ${error.message}`); + client.emit('error', 'Failed to kick player'); + } + } } diff --git a/be/gameServer/src/modules/rooms/rooms.websocket.emit.controller.ts b/be/gameServer/src/modules/rooms/rooms.websocket.emit.controller.ts index 1900d07..bc28f43 100644 --- a/be/gameServer/src/modules/rooms/rooms.websocket.emit.controller.ts +++ b/be/gameServer/src/modules/rooms/rooms.websocket.emit.controller.ts @@ -16,14 +16,8 @@ export class RoomsWebSocketEmitController { description: 'Room created successfully', type: RoomDataDto, }) - createRoom(): RoomDataDto { - return { - roomId: 'example-room-id', - roomName: 'example-room-name', - hostNickname: 'example-room-name', - players: [{ playerNickname: 'hostNickname', isReady: false }], - status: 'waiting', - }; + createRoom() { + return; } @Post('updateUsers') @@ -32,12 +26,27 @@ export class RoomsWebSocketEmitController { description: '방의 사용자들에게 "updateUsers" 이벤트를 통해 갱신된 사용자 목록을 제공합니다.', }) - updateUsers(): PlayerDataDto[] { + @ApiResponse({ + description: '플레이어 데이터 객체 배열', + type: [PlayerDataDto], + }) + updateUsers() { + // This method does not execute any logic. It's for Swagger documentation only. + return; + } + + @Post('kicked') + @ApiOperation({ + summary: '강퇴 이벤트 발생', + description: '방의 사용자들에게 다른 사용자의 강퇴 이벤트를 알립니다.', + }) + @ApiResponse({ + description: '해당 강퇴 사용자 닉네임', + type: String, + }) + kicked() { // This method does not execute any logic. It's for Swagger documentation only. - return [ - { playerNickname: 'hostNickname', isReady: true }, - { playerNickname: 'Player1', isReady: false }, - ]; + return; } @Post('error') diff --git a/be/gameServer/src/modules/rooms/rooms.websocket.on.controller.ts b/be/gameServer/src/modules/rooms/rooms.websocket.on.controller.ts index 0554256..43fad7a 100644 --- a/be/gameServer/src/modules/rooms/rooms.websocket.on.controller.ts +++ b/be/gameServer/src/modules/rooms/rooms.websocket.on.controller.ts @@ -1,9 +1,7 @@ import { Controller, Post, Body } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBody } from '@nestjs/swagger'; import { CreateRoomDto } from './dto/create-room.dto'; -import { RoomDataDto } from './dto/room-data.dto'; import { JoinRoomDto } from './dto/join-data.dto'; -import { PlayerDataDto } from '../players/dto/player-data.dto'; @ApiTags('Rooms (WebSocket: 서버에서 수신하는 이벤트)') @Controller('rooms') @@ -15,15 +13,9 @@ export class RoomsWebSocketOnController { 'wss://clovapatra.com/rooms 에서 "createRoom" 이벤트를 emit해 사용합니다. 성공적으로 게임방이 생성되면 "roomCreated" 이벤트를 발행해 RoomData를 전달합니다.', }) @ApiBody({ type: CreateRoomDto }) - createRoom(@Body() createRoomDto: CreateRoomDto): RoomDataDto { + createRoom(@Body() createRoomDto: CreateRoomDto) { // This method does not execute any logic. It's for Swagger documentation only. - return { - roomId: 'example-room-id', - roomName: createRoomDto.roomName, - hostNickname: createRoomDto.hostNickname, - players: [{ playerNickname: createRoomDto.hostNickname, isReady: false }], - status: 'waiting', - }; + return createRoomDto; } @Post('joinRoom') @@ -33,11 +25,8 @@ export class RoomsWebSocketOnController { 'wss://clovapatra.com/rooms 에서 "joinRoom" 이벤트를 emit해 사용합니다. 성공적으로 입장하면 입장한 방의 사용자들에게 "updateUsers" 이벤트를 통해 갱신된 사용자 목록을 제공합니다.', }) @ApiBody({ type: JoinRoomDto }) - joinRoom(@Body() joinRoomDto: JoinRoomDto): PlayerDataDto[] { - return [ - { playerNickname: 'example-creator-nickname', isReady: false }, - { playerNickname: joinRoomDto.playerNickname, isReady: false }, - ]; + joinRoom(@Body() joinRoomDto: JoinRoomDto) { + return joinRoomDto; } @Post('disconnect') @@ -48,30 +37,24 @@ export class RoomsWebSocketOnController { }) disconnect(): void {} - // @Post('kickPlayer') - // @ApiOperation({ - // summary: '플레이어 강퇴', - // description: - // 'wss://clovapatra.com/rooms 에서 "kickPlayer" 이벤트를 emit해 사용합니다. 성공적으로 강퇴되면 모든 클라이언트에게 "updateUsers" 이벤트를 발행합니다.', - // }) - // @ApiBody({ type: String }) - // kickPlayer(@Body() playerNickname: string): PlayerDataDto[] { - // return [ - // { playerNickname: 'example-creator-nickname', isReady: false }, - // { playerNickname: playerNickname, isReady: false }, - // ]; - // } @Post('setReady') @ApiOperation({ summary: '플레이어 준비 완료', description: - 'wss://clovapatra.com/rooms 에서 "setReady" 이벤트를 emit해 사용합니다. 성공적으로 처리되면 모든 클라이언트에게 "updateUsers" 이벤트를 발행합니다.', + 'wss://clovapatra.com/rooms 에서 "setReady" 이벤트를 emit해 사용합니다. 이미 준비 완료라면 준비대기로 변하고, 준비 대기라면 준비 완료 상태로 바뀝니다. 성공적으로 처리되면 모든 클라이언트에게 "updateUsers" 이벤트를 발행합니다.', }) - @ApiBody({ type: String }) - ready(@Body() playerNickname: string): PlayerDataDto[] { - return [ - { playerNickname: 'example-creator-nickname', isReady: false }, - { playerNickname: playerNickname, isReady: false }, - ]; + ready() { + return; + } + + @Post('kickPlayer') + @ApiOperation({ + summary: '플레이어 강퇴', + description: + 'wss://clovapatra.com/rooms 에서 "kickPlayer" 이벤트를 emit해 사용합니다. 성공적으로 강퇴되면 해당 방의 클라이언트에게 "kicked", "updateUsers" 이벤트를 발행합니다.', + }) + @ApiBody({ type: String, description: '강퇴할 사용자 playerNickname' }) + kickPlayer(@Body() playerNickname: string) { + return playerNickname; } }