From 57cc30966a0d11cdd19ad7798caec6ce26c9ca00 Mon Sep 17 00:00:00 2001 From: jiyun Date: Mon, 11 Dec 2023 20:01:35 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20channel=20gateway,=20game=20gateway=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auth/auth.controller.ts | 2 +- src/auth/auth.module.ts | 1 - src/channels/channels.gateway.ts | 38 ++++++++++- src/channels/channels.module.ts | 1 + src/game/dto/create-game-param.dto.ts | 4 ++ src/game/dto/gateway-join-room-param.dto.ts | 5 ++ src/game/game-invitation.repository.ts | 22 +++++++ src/game/game.controller.ts | 51 +++++++++++++-- src/game/game.gateway.ts | 72 +++++++++++---------- src/game/game.module.ts | 18 +++++- src/game/game.repository.ts | 22 +------ src/game/game.service.ts | 60 +++++++++++++++-- 12 files changed, 225 insertions(+), 71 deletions(-) create mode 100644 src/game/dto/create-game-param.dto.ts create mode 100644 src/game/dto/gateway-join-room-param.dto.ts diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 04e2d7a..e382442 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -13,7 +13,7 @@ import { Response } from 'express'; import { User } from 'src/users/entities/user.entity'; import { SignupRequestDto } from '../users/dto/signup-request.dto'; import { UsersService } from '../users/users.service'; -import { AppService } from './../app.service'; +import { AppService } from '../app.service'; import { AuthService } from './auth.service'; import { UserSigninResponseDto } from './dto/user-signin-response.dto'; import { FtAuthService } from './ft-auth.service'; diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 27007b7..4f031d0 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -52,7 +52,6 @@ import { JwtRefreshStrategy } from './jwt-refresh.strategy'; GameInvitationRepository, FriendsRepository, BlocksRepository, - ChannelsGateway, ChannelUsersRepository, FriendsRepository, AppService, diff --git a/src/channels/channels.gateway.ts b/src/channels/channels.gateway.ts index 940f8df..0b76944 100644 --- a/src/channels/channels.gateway.ts +++ b/src/channels/channels.gateway.ts @@ -14,13 +14,14 @@ import Redis from 'ioredis'; import { Server, Socket } from 'socket.io'; import { AuthService } from 'src/auth/auth.service'; import { UserStatus } from 'src/common/enum'; -import { EVENT_USER_STATUS } from 'src/common/events'; +import { EVENT_GAME_INVITATION, EVENT_USER_STATUS } from 'src/common/events'; import { WSBadRequestException } from 'src/common/exception/custom-exception'; import { WsExceptionFilter } from 'src/common/exception/custom-ws-exception.filter'; import { FriendsRepository } from 'src/users/friends.repository'; import { UsersRepository } from 'src/users/users.repository'; import { ChannelUsersRepository } from './channel-users.repository'; import { EventMessageOnDto } from './dto/event-message-on.dto'; +import { GatewayCreateInvitationParamDto } from '../game/dto/gateway-create-invitation-param.dto'; @WebSocketGateway({ namespace: 'channels' }) @UseFilters(WsExceptionFilter) @@ -194,4 +195,39 @@ export class ChannelsGateway this.logger.log('ClientToServer: ', data); this.server.emit('ServerToClient', 'Hello Client!'); } + + async inviteGame( + gatewayInvitationParamDto: GatewayCreateInvitationParamDto, + ) { + const invitationId = gatewayInvitationParamDto.invitationId; + const invitingUserNickname = + gatewayInvitationParamDto.invitingUserNickname; + const invitedUserId = gatewayInvitationParamDto.invitedUserId; + + const invitedUser = await this.usersRepository.findOne({ + where: { + id: invitedUserId, + }, + }); + if (!invitedUser) { + throw WSBadRequestException(`user ${invitedUserId} does not exist`); + } + + // 상대가 접속 중인지 => 접속 중인 유저가 아니라면 없던 일이 됨 + if ( + invitedUser.status === UserStatus.OFFLINE || + !invitedUser.channelSocketId + ) + throw WSBadRequestException('invited user is now offline'); + + console.log(`inviting user nickname: ${invitingUserNickname}`); + + this.server + .to(invitedUser.channelSocketId) + .emit(EVENT_GAME_INVITATION, { + invitationId: invitationId, + invitingUserNickname: invitingUserNickname, + gameType: gatewayInvitationParamDto.gameType, + }); + } } diff --git a/src/channels/channels.module.ts b/src/channels/channels.module.ts index 2083862..df474d2 100644 --- a/src/channels/channels.module.ts +++ b/src/channels/channels.module.ts @@ -52,5 +52,6 @@ import { Channel } from './entities/channel.entity'; FriendsRepository, BlocksRepository, ], + exports: [ChannelsGateway], }) export class ChannelsModule {} diff --git a/src/game/dto/create-game-param.dto.ts b/src/game/dto/create-game-param.dto.ts new file mode 100644 index 0000000..4d8ba8a --- /dev/null +++ b/src/game/dto/create-game-param.dto.ts @@ -0,0 +1,4 @@ +export class CreateGameParamDto { + invitedUserId: number; + invitationId: number; +} diff --git a/src/game/dto/gateway-join-room-param.dto.ts b/src/game/dto/gateway-join-room-param.dto.ts new file mode 100644 index 0000000..da75492 --- /dev/null +++ b/src/game/dto/gateway-join-room-param.dto.ts @@ -0,0 +1,5 @@ +export class GatewayJoinRoomParamDto { + invitingUserId: number; + invitedUserId: number; + roomName: string; +} diff --git a/src/game/game-invitation.repository.ts b/src/game/game-invitation.repository.ts index 73e3b3a..b817228 100644 --- a/src/game/game-invitation.repository.ts +++ b/src/game/game-invitation.repository.ts @@ -1,6 +1,8 @@ import { DataSource, Repository } from 'typeorm'; import { GameInvitation } from './entities/game-invitation.entity'; import { InjectRepository } from '@nestjs/typeorm'; +import { CreateGameInvitationParamDto } from './dto/create-invitation-param.dto'; +import { DBUpdateFailureException } from '../common/exception/custom-exception'; export class GameInvitationRepository extends Repository { constructor( @@ -8,4 +10,24 @@ export class GameInvitationRepository extends Repository { ) { super(GameInvitation, dataSource.manager); } + + async createGameInvitation( + gameInvitationDto: CreateGameInvitationParamDto, + ) { + const gameInvitation = this.create({ + invitingUserId: gameInvitationDto.invitingUser.id, + invitedUserId: gameInvitationDto.invitedUserId, + gameType: gameInvitationDto.gameType, + }); + const result = await this.save(gameInvitation); + if (!result) + throw DBUpdateFailureException('create game invitation failed'); + return result; + } + + async deleteGameInvitaiton(invitationId: number) { + const result = await this.softDelete(invitationId); + if (result.affected !== 1) + throw DBUpdateFailureException('delete user from channel failed'); + } } diff --git a/src/game/game.controller.ts b/src/game/game.controller.ts index f2a598d..6a49c4a 100644 --- a/src/game/game.controller.ts +++ b/src/game/game.controller.ts @@ -1,11 +1,20 @@ -import { Body, Controller, Post, UseGuards } from '@nestjs/common'; -import { AuthGuard } from '@nestjs/passport'; -import { ApiTags } from '@nestjs/swagger'; +import { + Body, + Controller, + Delete, + ParseIntPipe, + Post, + UseGuards, +} from '@nestjs/common'; +import { GameService } from './game.service'; +import { CreateInvitationRequestDto } from './dto/create-invitation-request.dto'; import { GetUser } from '../auth/get-user.decorator'; import { User } from '../users/entities/user.entity'; +import { ApiTags } from '@nestjs/swagger'; import { CreateGameInvitationParamDto } from './dto/create-invitation-param.dto'; -import { CreateInvitationRequestDto } from './dto/create-invitation-request.dto'; -import { GameService } from './game.service'; +import { PositiveIntPipe } from '../common/pipes/positiveInt.pipe'; +import { CreateGameParamDto } from './dto/create-game-param.dto'; +import { AuthGuard } from '@nestjs/passport'; @Controller('game') @ApiTags('game') @@ -27,5 +36,35 @@ export class GameController { await this.gameService.createInvitation(invitationParamDto); } - // @Post('/accept') + /** + * 초대 수락 + * @param user + * @param invitationId 초대 테이블의 id + * (초대시 프론트가 이벤트를 수신하며 받은 data 입니다) + */ + @Post('/accept') + async createGame( + @GetUser() user: User, + @Body('invitationId', ParseIntPipe, PositiveIntPipe) + invitationId: number, + ) { + const createGameParamDto: CreateGameParamDto = { + invitedUserId: user.id, + invitationId: invitationId, + }; + await this.gameService.createGame(createGameParamDto); + } + + @Delete('/reject') + async deleteInvitation( + @GetUser() user: User, + @Body('invitationId', ParseIntPipe, PositiveIntPipe) + invitationId: number, + ) { + const deleteInvitationParamDto: CreateGameParamDto = { + invitedUserId: user.id, + invitationId: invitationId, + }; + await this.gameService.deleteInvitation(deleteInvitationParamDto); + } } diff --git a/src/game/game.gateway.ts b/src/game/game.gateway.ts index 6270566..65f63b7 100644 --- a/src/game/game.gateway.ts +++ b/src/game/game.gateway.ts @@ -8,15 +8,14 @@ import { import { UsersRepository } from '../users/users.repository'; import { Server, Socket } from 'socket.io'; import { AuthService } from '../auth/auth.service'; -import { GatewayCreateInvitationParamDto } from './dto/gateway-create-invitation-param.dto'; -import { UserStatus } from '../common/enum'; import { WSBadRequestException } from '../common/exception/custom-exception'; -import { EVENT_GAME_INVITATION } from '../common/events'; -import { UseFilters } from '@nestjs/common'; +import { UseFilters, UsePipes, ValidationPipe } from '@nestjs/common'; import { WsExceptionFilter } from '../common/exception/custom-ws-exception.filter'; +import { GatewayJoinRoomParamDto } from './dto/gateway-join-room-param.dto'; +import { GameInvitation } from './entities/game-invitation.entity'; -@UseFilters(WsExceptionFilter) @WebSocketGateway({ namespace: 'game' }) +@UseFilters(WsExceptionFilter) export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect { constructor( private readonly authService: AuthService, @@ -25,6 +24,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; + private connectedClients: Map = new Map(); async handleConnection(@ConnectedSocket() client: Socket) { const user = await this.authService.getUserFromSocket(client); @@ -37,47 +37,53 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect { await this.usersRepository.update(user.id, { gameSocketId: client.id, }); + this.connectedClients.set(user.id, client); } async handleDisconnect(@ConnectedSocket() client: Socket) { const user = await this.authService.getUserFromSocket(client); - if (!user || !client.id || user.gameSocketId) return; + if (!user || client.id !== user.gameSocketId) return; await this.usersRepository.update(user.id, { gameSocketId: null, }); + this.connectedClients.delete(user.id); client.rooms.clear(); } - async inviteGame( - gatewayInvitationParamDto: GatewayCreateInvitationParamDto, - ) { - const invitationId = gatewayInvitationParamDto.invitationId; - const invitingUserNickname = - gatewayInvitationParamDto.invitingUserNickname; - const invitedUserId = gatewayInvitationParamDto.invitedUserId; + gameSetting(invitation: GameInvitation) { + // 1. Game DB 생성하기 - const invitedUser = await this.usersRepository.findOne({ - where: { - id: invitedUserId, - }, - }); - if (!invitedUser) { - throw WSBadRequestException(`user ${invitedUserId} does not exist`); - } + // 2. 초대 해 준 사람이랑 같이 room에 들어가기 + const invitingUserId = invitation.invitingUserId; + const invitedUserId = invitation.invitedUserId; + const gatewayJoinRoomParamDto: GatewayJoinRoomParamDto = { + invitingUserId: invitingUserId, + invitedUserId: invitedUserId, + roomName: `${invitingUserId}:${invitedUserId}`, + }; + this.joinGameRoom(gatewayJoinRoomParamDto); - // 상대가 접속 중인지 => 접속 중인 유저가 아니라면 없던 일이 됨 - if ( - invitedUser.status === UserStatus.OFFLINE || - !invitedUser.gameSocketId - ) - throw WSBadRequestException('invited user is now offline'); + // game start emit ? + } - console.log(`invitation id: ${invitationId}`); - console.log(`invited user gameSocketId: ${invitedUser.gameSocketId}`); - this.server.to(invitedUser.gameSocketId).emit(EVENT_GAME_INVITATION, { - invitationId: invitationId, - invitingUserNickname: invitingUserNickname, - }); + joinGameRoom(gatewayJoinRoomParamDto: GatewayJoinRoomParamDto) { + const invitingUserId = gatewayJoinRoomParamDto.invitingUserId; + const invitedUserId = gatewayJoinRoomParamDto.invitedUserId; + const room = gatewayJoinRoomParamDto.roomName; + + const invitingUserSocket = this.connectedClients.get(invitingUserId); + if (!invitingUserSocket) + throw WSBadRequestException( + `${invitingUserId} 는 연결되지 않은 클라이언트입니다`, + ); + const invitedUserSocket = this.connectedClients.get(invitedUserId); + if (!invitedUserSocket) + throw WSBadRequestException( + `${invitedUserId} 는 연결되지 않은 클라이언트입니다`, + ); + + invitingUserSocket.join(room); + invitedUserSocket.join(room); } } diff --git a/src/game/game.module.ts b/src/game/game.module.ts index cd0d877..ebd83c9 100644 --- a/src/game/game.module.ts +++ b/src/game/game.module.ts @@ -11,15 +11,31 @@ import { GameInvitation } from './entities/game-invitation.entity'; import { GameGateway } from './game.gateway'; import { AuthService } from '../auth/auth.service'; import { JwtService } from '@nestjs/jwt'; +import { ChannelUsersRepository } from '../channels/channel-users.repository'; +import { FriendsRepository } from '../users/friends.repository'; +import { ChannelUser } from '../channels/entities/channel-user.entity'; +import { Friend } from '../users/entities/friend.entity'; +import { ChannelsModule } from '../channels/channels.module'; @Module({ - imports: [TypeOrmModule.forFeature([Game, GameInvitation, User])], + imports: [ + TypeOrmModule.forFeature([ + Game, + GameInvitation, + User, + ChannelUser, + Friend, + ]), + ChannelsModule, + ], controllers: [GameController], providers: [ GameGateway, GameService, AuthService, JwtService, + ChannelUsersRepository, + FriendsRepository, GameRepository, GameInvitationRepository, UsersRepository, diff --git a/src/game/game.repository.ts b/src/game/game.repository.ts index 3d4a7da..ee24880 100644 --- a/src/game/game.repository.ts +++ b/src/game/game.repository.ts @@ -2,14 +2,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { GAME_DEFAULT_PAGE_SIZE } from 'src/common/constants'; import { DataSource, Repository } from 'typeorm'; import { Game } from './entities/game.entity'; -import { CreateGameInvitationParamDto } from './dto/create-invitation-param.dto'; -import { DBUpdateFailureException } from '../common/exception/custom-exception'; -import { GameInvitationRepository } from './game-invitation.repository'; export class GameRepository extends Repository { - constructor( - @InjectRepository(Game) private dataSource: DataSource, - private readonly gameInvitationRepository: GameInvitationRepository, - ) { + constructor(@InjectRepository(Game) private dataSource: DataSource) { super(Game, dataSource.manager); } @@ -44,18 +38,4 @@ export class GameRepository extends Repository { ); return gameHistories; } - - async createGameInvitation( - gameInvitationDto: CreateGameInvitationParamDto, - ) { - const gameInvitation = this.gameInvitationRepository.create({ - invitingUserId: gameInvitationDto.invitingUser.id, - invitedUserId: gameInvitationDto.invitedUserId, - gameType: gameInvitationDto.gameType, - }); - const result = await this.gameInvitationRepository.save(gameInvitation); - if (!result) - throw DBUpdateFailureException('create game invitation failed'); - return result; - } } diff --git a/src/game/game.service.ts b/src/game/game.service.ts index f467cd6..d394e18 100644 --- a/src/game/game.service.ts +++ b/src/game/game.service.ts @@ -1,15 +1,24 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { + BadRequestException, + ImATeapotException, + Injectable, +} from '@nestjs/common'; import { GameRepository } from './game.repository'; import { UsersRepository } from '../users/users.repository'; import { CreateGameInvitationParamDto } from './dto/create-invitation-param.dto'; import { GatewayCreateInvitationParamDto } from './dto/gateway-create-invitation-param.dto'; import { GameGateway } from './game.gateway'; +import { CreateGameParamDto } from './dto/create-game-param.dto'; +import { GameInvitationRepository } from './game-invitation.repository'; +import { ChannelsGateway } from '../channels/channels.gateway'; @Injectable() export class GameService { constructor( private readonly gameRepository: GameRepository, + private readonly gameInvitationRepository: GameInvitationRepository, private readonly gameGateway: GameGateway, + private readonly channelsGateway: ChannelsGateway, private readonly usersRepository: UsersRepository, ) {} @@ -33,18 +42,55 @@ export class GameService { ); } - // db 저장 - const gameInvitation = await this.gameRepository.createGameInvitation( - invitationParamDto, - ); + // Game Invitation DB 저장 + const gameInvitation = + await this.gameInvitationRepository.createGameInvitation( + invitationParamDto, + ); // TODO: 알림 보내기 const gatewayInvitationParamDto: GatewayCreateInvitationParamDto = { invitationId: gameInvitation.id, - invitingUserNickname: invitedUser.nickname, + invitingUserNickname: invitingUser.nickname, invitedUserId: invitedUserId, gameType: invitationParamDto.gameType, }; - await this.gameGateway.inviteGame(gatewayInvitationParamDto); + await this.channelsGateway.inviteGame(gatewayInvitationParamDto); + } + + async createGame(createGameParamDto: CreateGameParamDto) { + const invitationId = createGameParamDto.invitationId; + const invitedUserId = createGameParamDto.invitedUserId; + + const invitation = await this.gameInvitationRepository.findOne({ + where: { + id: invitationId, + invitedUserId: invitedUserId, + }, + }); + if (!invitation) + return new ImATeapotException( + `invitation id ${invitationId}는 존재하지 않습니다`, + ); + // Game setting + } + + async deleteInvitation(deleteInvitationParamDto: CreateGameParamDto) { + const invitationId = deleteInvitationParamDto.invitationId; + const invitedUserId = deleteInvitationParamDto.invitedUserId; + + const invitation = await this.gameInvitationRepository.findOne({ + where: { + id: invitationId, + invitedUserId: invitedUserId, + }, + }); + if (!invitation) + return new ImATeapotException( + `invitation id ${invitationId}는 존재하지 않습니다`, + ); + + // invitation DB 지우기 + await this.gameInvitationRepository.deleteGameInvitaiton(invitationId); } }