Skip to content

Commit

Permalink
[FE] 사용자는 게임방에 입장할 수 있다. (#65)
Browse files Browse the repository at this point in the history
* feat: 게임방 입장(joinRoom) Socket 이벤트 함수 구현

* chore: 변수명 변경

* feat: 사용자 게임방 입장 시 상태 변경

* chore: Socket ID console.log에서 제거
  • Loading branch information
studioOwol authored Nov 7, 2024
1 parent 048a8e9 commit aabdeca
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 48 deletions.
33 changes: 0 additions & 33 deletions fe/src/api/socketApi.ts

This file was deleted.

24 changes: 13 additions & 11 deletions fe/src/pages/RoomPage/RoomList/JoinDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,36 @@ import {
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import useRoomStore from '@/store/useRoomStore';
import { JoinDialogProps } from '@/types/roomTypes';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

const JoinDialog = ({ open, onOpenChange, roomId }: JoinDialogProps) => {
const [nickname, setNickname] = useState('');
const [playerNickname, setPlayerNickname] = useState('');
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const joinGameRoom = useRoomStore((state) => state.joinGameRoom);

const resetAndClose = () => {
setNickname('');
setPlayerNickname('');
setIsLoading(false);
onOpenChange(false);
};

const handleJoin = async () => {
if (!nickname.trim()) return;
if (!playerNickname.trim()) return;

try {
setIsLoading(true);
// TODO: 방 입장 로직 구현
// await joinRoom(roomId, nickname.trim());
joinGameRoom(roomId, playerNickname.trim());

// stream은 별도의 상태 관리 필요 (예: audio store)

resetAndClose();
navigate(`/game/${roomId}`);
} catch (error) {
console.error('방 입장 실패:', error);
// TODO: 에러 처리 토스트 메시지로
} finally {
setIsLoading(false);
}
Expand All @@ -51,13 +53,13 @@ const JoinDialog = ({ open, onOpenChange, roomId }: JoinDialogProps) => {
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="nickname" className="text-right">
<Label htmlFor="playerNickname" className="text-right">
닉네임
</Label>
<Input
id="nickname"
value={nickname}
onChange={(e) => setNickname(e.target.value)}
id="playerNickname"
value={playerNickname}
onChange={(e) => setPlayerNickname(e.target.value)}
placeholder="닉네임을 입력하세요"
className="col-span-3"
disabled={isLoading}
Expand All @@ -76,7 +78,7 @@ const JoinDialog = ({ open, onOpenChange, roomId }: JoinDialogProps) => {
<Button
type="button"
onClick={handleJoin}
disabled={!nickname.trim() || isLoading}
disabled={!playerNickname.trim() || isLoading}
>
입장하기
</Button>
Expand Down
113 changes: 113 additions & 0 deletions fe/src/services/gameSocket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { JoinGameRoomResult, Room } from '@/types/roomTypes';
import {
ClientToServerEvents,
ServerToClientEvents,
} from '@/types/socketTypes';
import { io, Socket } from 'socket.io-client';

const SOCKET_BASE_URL = 'wss://game.clovapatra.com';

const gameSocket: Socket<ServerToClientEvents, ClientToServerEvents> = io(
`${SOCKET_BASE_URL}/rooms`,
{
transports: ['websocket'],
withCredentials: true,
}
);

// 소켓 연결 상태 모니터링
gameSocket.on('connect', () => {
console.log('Socket connected');
});

gameSocket.on('connect_error', (error) => {
console.error('Socket connection error:', error);
});

gameSocket.on('disconnect', (reason) => {
console.log('Socket disconnected:', reason);
});

export const createRoom = async (
roomName: string,
hostNickname: string
): Promise<Room> => {
return new Promise((resolve, reject) => {
gameSocket.emit('createRoom', { roomName, hostNickname });

gameSocket.on('roomCreated', (room) => {
resolve(room);
});

gameSocket.on('error', (error) => {
reject(error);
});
});
};

export const joinRoom = async (
roomId: string,
playerNickname: string
): Promise<JoinGameRoomResult> => {
if (!gameSocket.connected) {
throw new Error('서버와 연결이 되지 않았습니다.');
}

console.log('Attempting to join room:', { roomId, playerNickname });

return new Promise((resolve, reject) => {
let isResolved = false;

// updateUsers 이벤트 핸들러
const handleUpdateUsers = (players: string[]) => {
console.log('Received updateUsers:', players);

// 첫 업데이트에서만 resolve 하도록
if (!isResolved) {
isResolved = true;
cleanup();

// Room 객체 구성
const room: Room = {
roomId,
roomName: `Room ${roomId}`, // 서버에서 따로 제공하지 않음
hostNickname: players[0], // 첫 번째 플레이어를 호스트로 가정
players: players,
status: 'waiting',
};

resolve({
room,
stream: new MediaStream(),
});
}
};

const handleError = (error: { code: string; message: string }) => {
console.error('Error joining room:', error);
cleanup();
reject(error);
};

const cleanup = () => {
gameSocket.off('updateUsers', handleUpdateUsers);
gameSocket.off('error', handleError);
clearTimeout(timeoutId);
};

// 이벤트 리스너 등록
gameSocket.on('updateUsers', handleUpdateUsers);
gameSocket.on('error', handleError);

// 방 입장 요청
gameSocket.emit('joinRoom', { roomId, playerNickname });

// 타임아웃 설정
const timeoutId = setTimeout(() => {
if (!isResolved) {
cleanup();
reject(new Error('서버 응답 시간이 초과되었습니다.'));
}
}, 5000);
});
};
25 changes: 24 additions & 1 deletion fe/src/store/useRoomStore.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createRoom } from '@/api/socketApi';
import { createRoom, joinRoom } from '@/services/gameSocket';
import { RoomStore } from '@/types/roomTypes';
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
Expand Down Expand Up @@ -26,6 +26,29 @@ const useRoomStore = create<RoomStore>()(
throw error;
}
},

joinGameRoom: async (roomId: string, playerNickname: string) => {
try {
const { room } = await joinRoom(roomId, playerNickname);

set((state) => {
const updatedRooms = state.rooms.map((existingRoom) =>
existingRoom.roomId === roomId
? { ...existingRoom, players: room.players }
: existingRoom
);

return {
rooms: updatedRooms,
currentRoom:
updatedRooms.find((room) => room.roomId === roomId) || null,
};
});
} catch (error) {
console.error('방 입장 실패:', error);
throw error;
}
},
}),
{
name: 'Room Store',
Expand Down
6 changes: 6 additions & 0 deletions fe/src/types/roomTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@ export interface PaginationProps {
onPageChange: (page: number) => void;
}

export interface JoinGameRoomResult {
room: Room;
stream: MediaStream;
}

export interface RoomStore {
rooms: Room[];
currentRoom: Room | null;
addRoom: (roomName: string, hostNickname: string) => Promise<string>;
joinGameRoom: (roomId: string, playerNickname: string) => void;
}
30 changes: 27 additions & 3 deletions fe/src/types/socketTypes.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,38 @@
import { Room } from './roomTypes';

// 게임 서버 이벤트 타입
export interface ServerToClientEvents {
roomCreated: (room: Room) => void;
roomJoined: (room: Room) => void;
roomLeft: (room: Room) => void;
updateUsers: (players: string[]) => void;
error: (error: { code: string; message: string }) => void;
}

export interface ClientToServerEvents {
createRoom: (data: { roomName: string; hostNickname: string }) => void;
joinRoom: (data: { roomId: string; userNickname: string }) => void;
joinRoom: (data: { roomId: string; playerNickname: string }) => void;
leaveRoom: (data: { roomId: string }) => void;
}

// 시그널링 서버 이벤트 타입
export interface SignalingServerToClientEvents {
joinSuccess: () => void;
'peer-joined': (data: { peerId: string; userId: string }) => void;
'peer-left': (data: { peerId: string }) => void;
offer: (data: { targetId: string; sdp: RTCSessionDescriptionInit }) => void;
answer: (data: { targetId: string; sdp: RTCSessionDescriptionInit }) => void;
'ice-candidate': (data: {
targetId: string;
candidate: RTCIceCandidateInit;
}) => void;
}

export interface SignalingClientToServerEvents {
join: (data: { roomId: string; userId: string }) => void;
leave: (data: { roomId: string }) => void;
offer: (data: { targetId: string; sdp: RTCSessionDescriptionInit }) => void;
answer: (data: { targetId: string; sdp: RTCSessionDescriptionInit }) => void;
'ice-candidate': (data: {
targetId: string;
candidate: RTCIceCandidateInit;
}) => void;
}

0 comments on commit aabdeca

Please sign in to comment.