Skip to content

Commit

Permalink
[BE - #] (#77)
Browse files Browse the repository at this point in the history
* chore: socket.io-client, uuid 라이브러리 설치

* feat: game  gateway 이벤트 추가
- 방장 접속
- 참여자 접속
- sid 쿠키 설정

* feat: master entry 이벤트 구현

* feat: participant entry 이벤트 구현

---------

Co-authored-by: dooohun <[email protected]>
  • Loading branch information
nowChae and dooohun authored Nov 18, 2024
1 parent 48cbad1 commit c81c4d5
Show file tree
Hide file tree
Showing 15 changed files with 237 additions and 22 deletions.
57 changes: 51 additions & 6 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
3 changes: 2 additions & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"qrcode.react": "^4.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.27.0"
"react-router-dom": "^6.27.0",
"socket.io-client": "^4.8.1"
},
"devDependencies": {
"@chromatic-com/storybook": "^3.2.2",
Expand Down
15 changes: 15 additions & 0 deletions packages/client/src/pages/nickname/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,26 @@ import AvatarIcon from '@/shared/assets/icons/avatar.svg?react';

import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { CustomButton } from '@/shared/ui/buttons';
import { getCookie, setCookie } from '@/shared/utils/cookie';
import { getQuizSocket } from '@/shared/utils/socket';

export default function Nickname() {
const [nickname, setNickname] = useState('');
const navigate = useNavigate();

const handleNicknameSubmit = (nickname: string) => {
const socket = getQuizSocket();
const sid = getCookie('sid');
if (sid) {
socket.emit('entry', { roomId: 123456, nickname: nickname, sid: sid });
return;
}
socket.emit('entry', { roomId: 123456, nickname: nickname });

socket.on('session', (response) => {
setCookie('sid', response.sid);
});
// TODO: API 연동 후 submit 함수 구현
console.log(nickname);
navigate('/quiz/wait');
Expand Down Expand Up @@ -50,6 +64,7 @@ export default function Nickname() {
}`}
onClick={() => handleNicknameSubmit(nickname)}
disabled={nickname.length === 0}
onClick={() => handleNicknameSubmit(nickname)}
>
Join
</button>
Expand Down
19 changes: 12 additions & 7 deletions packages/client/src/pages/quiz-wait/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { generateRandomPositions } from '@/shared/utils/generateRandomPositions'
import { QRCodeSVG } from 'qrcode.react';
import { useLayoutEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { getQuizSocket } from '@/shared/utils/socket';
import { getCookie } from '@/shared/utils/cookie';

// TODO: 파일 분리
const GUEST_DISPLAY_SIZE = { width: 1020, height: 576 };
Expand All @@ -11,16 +13,21 @@ const BUTTON_SIZE = { width: 74, height: 44 };

// TODO: API 연동 후 삭제
const fakeLink = 'https://google.com';
const fakeGuests = ['도훈', '성현', '병찬', '채원'];

const from = { x: SPACING, y: SPACING };
const to = { x: GUEST_DISPLAY_SIZE.width - SPACING, y: GUEST_DISPLAY_SIZE.height - SPACING };
const count = fakeGuests.length;

export default function QuizWait() {
const buttonRefs = useRef<HTMLDivElement[]>([]);
const [buttonSize, setButtonSize] = useState(BUTTON_SIZE);
const navigate = useNavigate();
const [guests, setGuests] = useState<string[]>([]);
const guestCount = guests.length;
const socket = getQuizSocket();

socket.on('nickname', (response) => {
setGuests((prev) => [response.nickname, ...prev]);
});

useLayoutEffect(() => {
if (buttonRefs.current.length > 0) {
Expand All @@ -34,7 +41,7 @@ export default function QuizWait() {
}
}, []);

const randomPositions = generateRandomPositions({ from, to, count, buttonSize });
const randomPositions = generateRandomPositions({ from, to, count: guestCount, buttonSize });

const handleCopyLink = () => {
try {
Expand All @@ -47,9 +54,7 @@ export default function QuizWait() {
};

const handleQuizStart = () => {
// DEMO 용도로 바로 퀴즈 페이지로 이동 추후 삭제
navigate('/quiz/session');
// TODO: socket.emit을 통해 전체 사용자에게 퀴즈 시작 이벤트 전달
socket.emit('master entry', { classId: '123', sid: getCookie('sid') });
};

return (
Expand Down Expand Up @@ -79,7 +84,7 @@ export default function QuizWait() {
}
}}
>
<CustomButton type="full" color="light" label={fakeGuests[index]} size="md" />
<CustomButton type="full" color="light" label={guests[index]} size="md" />
</div>
))}
</div>
Expand Down
21 changes: 21 additions & 0 deletions packages/client/src/shared/utils/cookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export function getCookie(name: string) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);

if (parts.length === 2) {
return parts.pop()?.split(';').shift();
}
return undefined;
}

export function setCookie(name: string, val: any, day?: number) {
const date = new Date();
const value = val;
// day가 없는 경우 세션쿠키로 설정
if (day) {
date.setTime(date.getTime() + day * 24 * 60 * 60 * 1000);
document.cookie = `${name}=${value}; expires=${date.toUTCString()}; path=/`;
} else {
document.cookie = `${name}=${value}; path=/`;
}
}
10 changes: 10 additions & 0 deletions packages/client/src/shared/utils/socket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { io, Socket } from 'socket.io-client';

let socket: Socket | null = null;

export function getQuizSocket(): Socket {
if (!socket) {
socket = io('http://localhost:3000/game');
}
return socket;
}
3 changes: 2 additions & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"socket.io": "^4.8.1",
"typeorm": "^0.3.20"
"typeorm": "^0.3.20",
"uuid": "^11.0.3"
},
"devDependencies": {
"@nestjs/testing": "^10.4.6",
Expand Down
3 changes: 2 additions & 1 deletion packages/server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { QuizModule } from './module/quiz/quiz.module';
import { GameGateway } from './module/game/game.gateway';
import { RedisService } from './config/database/redis/redis.service';
import { RedisModule } from '@nestjs-modules/ioredis'; // 추가
import { GameService } from './module/game/games/game.service';
@Module({
imports: [
UserModule,
Expand All @@ -36,7 +37,7 @@ import { RedisModule } from '@nestjs-modules/ioredis'; // 추가
}),
],
controllers: [AppController],
providers: [AppService, GameGateway, RedisService],
providers: [AppService, GameGateway, RedisService, GameService],
exports: [RedisService],
})
export class AppModule {}
66 changes: 60 additions & 6 deletions packages/server/src/module/game/game.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import {
OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { RedisService } from '../../config/database/redis/redis.service';
import { v4 as uuidv4 } from 'uuid';
import { GameService } from './games/game.service';

@WebSocketGateway({
cors: {
Expand All @@ -17,7 +20,10 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;

constructor() {}
constructor(
private readonly redisService: RedisService,
private readonly gameService: GameService,
) {}
// 클라이언트가 연결했을 때 처리하는 메서드
async handleConnection(client: Socket) {
console.log(`Client connected: ${client.id}`);
Expand All @@ -28,10 +34,58 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect {
console.log(`Client disconnected: ${client.id}`);
}

// 클라이언트로부터 메시지를 받을 때 처리하는 메서드
@SubscribeMessage('message')
handleMessage(client: Socket, payload: any): string {
console.log('Received message:', payload, client.id);
return 'server의 응답';
@SubscribeMessage('Quiz')
handleQuiz(client: Socket, payload: any) {
// classID, 퀴즈의 정보를 가져온다.
const quiz = this.redisService.get(payload.classId);
}

@SubscribeMessage('submit')
handleSubmit() {}

@SubscribeMessage('master entry')
async handleMasterEntry(client: Socket, payload: any) {
// 방장이 게임을 나가도 재접속이 가능하며, 게임은 지속된다.

const masterSid = uuidv4();
const pinCode = uuidv4().slice(0, 6); // 메소드 분리해서 중복 확인하고 없을 때까지 반복

client.join(pinCode); // pinCode로 되어 있는 roomd을 들어감

this.redisService.set(`master_sid=${masterSid}`, pinCode);

const { classId } = payload;
const gameInfo = { classId, currentOrder: 0, participantList: [] };

this.redisService.set(`gameId=${pinCode}`, JSON.stringify(gameInfo));

// refactor: 캐싱이 되어있다면 기간 연장, 안되어있다면 MySQL에서 데이터 가져오기, 데이터 전처리
const quizData = await this.gameService.cachingQuizData(classId);
this.redisService.set(`classId=${classId}`, JSON.stringify(quizData));

client.emit('session', masterSid);
client.emit('pincode', pinCode);
}

@SubscribeMessage('participant entry')
async handleParticipantEntry(client: Socket, payload: any) {
const { pinCode, nickname } = payload;
const clientInfo = { pinCode, nickname };

client.join(pinCode); // 같은 룸에 들어오게 된것

const participantSid = uuidv4();
this.redisService.set(`participant_sid=${participantSid}`, JSON.stringify(clientInfo));
client.emit('session', participantSid);

// 레디스 갱신
const gameInfo = JSON.parse(await this.redisService.get(`gameId=${pinCode}`));
const newParticipantList = gameInfo.participantList.push(nickname);
this.redisService.set(`gameId=${pinCode}`, JSON.stringify(gameInfo));

client.emit('nickname', newParticipantList);
client.to(pinCode).emit('nickname', newParticipantList);

return;
}
}
19 changes: 19 additions & 0 deletions packages/server/src/module/game/games/game.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Injectable, HttpException, HttpStatus, Param } from '@nestjs/common';
import { ChoiceRepository } from '../../quiz/quizzes/repositories/choice.repository';
import { ClassRepository } from '../../quiz/quizzes/repositories/class.repository';
import { QuizRepository } from '../../quiz/quizzes/repositories/quiz.repository';

@Injectable()
export class GameService {
constructor(
private readonly classRepository: ClassRepository,
private readonly quizRepository: QuizRepository,
private readonly choiceRepository: ChoiceRepository,
) {}

async cachingQuizData(classId: number) {
const classWithRelations = await this.classRepository.findClassWithRelations(classId);

return classWithRelations;
}
}
Loading

0 comments on commit c81c4d5

Please sign in to comment.