Skip to content

Commit

Permalink
[FE] 방장은 다른 참가자를 강제퇴장 시킬 수 있다. (#129)
Browse files Browse the repository at this point in the history
* chore: 디버깅용 console.log 제거

* chore: react-toastify 설치 및  ToastContainer App.tsx에 추가

* feat: 강퇴 관련 타입, 상태, 이벤트 설정

* feat: 강퇴 기능 구현

* feat: 강퇴 버튼을 클릭하면 KickDialog 띄우기

* style: 토스트 메시지 폰트 변경
  • Loading branch information
studioOwol authored Nov 20, 2024
1 parent 894af96 commit eac257f
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 37 deletions.
68 changes: 68 additions & 0 deletions fe/package-lock.json

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

1 change: 1 addition & 0 deletions fe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"react-router-dom": "^6.27.0",
"react-toastify": "^10.0.6",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
Expand Down
3 changes: 3 additions & 0 deletions fe/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import './App.css';
import GamePage from './pages/GamePage';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import RoomListPage from './pages/RoomListPage';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

const queryClient = new QueryClient();

Expand All @@ -16,6 +18,7 @@ function App() {
<Route path="/game/:roomId" element={<GamePage />} />
</Routes>
</BrowserRouter>
<ToastContainer />
</div>
</QueryClientProvider>
);
Expand Down
8 changes: 0 additions & 8 deletions fe/src/hooks/useAudioManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useCallback } from 'react';

export const useAudioManager = () => {
const setAudioStream = useCallback((peerId: string, stream: MediaStream) => {
console.log('setAudioStream 호출됨:', peerId);
const existingAudio = document.getElementById(
`audio-${peerId}`
) as HTMLAudioElement;
Expand All @@ -18,22 +17,15 @@ export const useAudioManager = () => {
audioElement.volume = 0.5; // 초기 볼륨

document.body.appendChild(audioElement);
console.log('오디오 엘리먼트 생성됨:', {
peerId,
volume: audioElement.volume,
});
}, []);

const setVolume = useCallback((peerId: string, volume: number) => {
console.log('1. setVolume 호출됨:', { peerId, volume });
const audioElement = document.getElementById(
`audio-${peerId}`
) as HTMLAudioElement;

if (audioElement) {
console.log('3. 볼륨 변경 전:', audioElement.volume);
audioElement.volume = volume;
console.log('4. 볼륨 변경 후:', audioElement.volume);
} else {
console.log('audioElement를 찾을 수 없음:', peerId);
}
Expand Down
53 changes: 53 additions & 0 deletions fe/src/pages/GamePage/GameDialog/KickDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// pages/GamePage/PlayerList/KickDialog.tsx
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { RoomDialogProps } from '@/types/roomTypes';
import { gameSocket } from '@/services/gameSocket';
import { cn } from '@/lib/utils';

interface KickDialogProps extends RoomDialogProps {
playerNickname: string;
}

const KickDialog = ({
open,
onOpenChange,
playerNickname,
}: KickDialogProps) => {
const handleKick = () => {
gameSocket.kickPlayer(playerNickname);
onOpenChange(false);
};

return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent className="font-galmuri sm:max-w-[22rem]">
<AlertDialogHeader>
<AlertDialogTitle>강제 퇴장 확인</AlertDialogTitle>
<AlertDialogDescription>
{playerNickname}님을 정말로 강제 퇴장 하시겠습니까?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>취소</AlertDialogCancel>
<AlertDialogAction
onClick={handleKick}
className={cn('bg-destructive hover:bg-destructive/90')}
>
확인
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

export default KickDialog;
11 changes: 9 additions & 2 deletions fe/src/pages/GamePage/PlayerList/Player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@ import useRoomStore from '@/stores/zustand/useRoomStore';
import { Button } from '@/components/ui/button';
import { useState } from 'react';
import { signalingSocket } from '@/services/signalingSocket';
import KickDialog from '../GameDialog/KickDialog';

const Player = ({ playerNickname, isReady }: PlayerProps) => {
const { currentRoom, currentPlayer } = useRoomStore();
const isCurrentPlayerHost = currentPlayer === currentRoom?.hostNickname;
const isPlayerHost = isHost(playerNickname);
const isCurrentPlayer = currentPlayer === playerNickname;
const [isMuted, setIsMuted] = useState(false);
const [showKickDialog, setShowKickDialog] = useState(false);

const handleKick = () => {
// TODO: 강퇴 로직 구현
console.log(`강퇴: ${playerNickname}`);
setShowKickDialog(true);
};

const handleMuteToggle = () => {
Expand Down Expand Up @@ -77,6 +78,12 @@ const Player = ({ playerNickname, isReady }: PlayerProps) => {
)}
</div>
</CardContent>

<KickDialog
open={showKickDialog}
onOpenChange={setShowKickDialog}
playerNickname={playerNickname}
/>
</Card>
);
};
Expand Down
19 changes: 18 additions & 1 deletion fe/src/pages/GamePage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ import { NotFound } from '@/components/common/NotFound';
import GameScreen from './GameScreen/GameScreen';
import { useAudioManager } from '@/hooks/useAudioManager';
import { signalingSocket } from '@/services/signalingSocket';
import { toast } from 'react-toastify';

const GamePage = () => {
const [showExitDialog, setShowExitDialog] = useState(false);
const { currentRoom } = useRoomStore();
const { currentRoom, kickedPlayer, setKickedPlayer } = useRoomStore();
const audioManager = useAudioManager();

useReconnect({ currentRoom });
useBackExit({ setShowExitDialog });

// 오디오 매니저 설정
useEffect(() => {
signalingSocket.setAudioManager(audioManager);

Expand All @@ -26,6 +28,21 @@ const GamePage = () => {
};
}, [audioManager]);

// 강퇴 알림 처리 추가
useEffect(() => {
if (kickedPlayer) {
toast.error(`${kickedPlayer}님이 강퇴되었습니다.`, {
position: 'bottom-right',
autoClose: 2000,
style: {
fontFamily: 'Galmuri11, monospace',
},
});

setKickedPlayer(null);
}
}, [kickedPlayer, setKickedPlayer, toast]);

const handleClickExit = () => {
setShowExitDialog(true);
};
Expand Down
25 changes: 25 additions & 0 deletions fe/src/services/gameSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,27 @@ class GameSocket extends SocketService {
});
}
});

this.socket.on('kicked', (playerNickname: string) => {
const {
currentRoom,
currentPlayer,
setCurrentRoom,
setCurrentPlayer,
setKickedPlayer,
} = useRoomStore.getState();

if (!currentRoom) return;

if (currentPlayer === playerNickname) {
setCurrentRoom(null);
setCurrentPlayer(null);
window.location.href = '/';
return;
}

setKickedPlayer(playerNickname);
});
}

createRoom(roomName: string, hostNickname: string) {
Expand All @@ -67,6 +88,10 @@ class GameSocket extends SocketService {
joinRoom(roomId: string, playerNickname: string) {
this.socket?.emit('joinRoom', { roomId, playerNickname });
}

kickPlayer(playerNickname: string) {
this.socket?.emit('kickPlayer', playerNickname);
}
}

export const gameSocket = new GameSocket();
27 changes: 1 addition & 26 deletions fe/src/services/signalingSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ class SignalingSocket extends SocketService {
// console.log('[WebRTCClient] 방 정보 수신:', roomInfo);
const { setUserMappings } = usePeerStore.getState();

console.log(roomInfo);
setUserMappings(roomInfo.userMappings);

this.handleRoomInfo();
Expand Down Expand Up @@ -379,35 +378,11 @@ class SignalingSocket extends SocketService {
*/
private handleRemoteStream(stream: MediaStream, peerId: string) {
// console.log('[WebRTCClient] 원격 스트림 처리:', peerId);
console.log('1. Remote Stream 수신:', {
peerId,
streamActive: stream.active,
audioTracks: stream.getAudioTracks().length,
});

const audioManager = this.audioManager;
console.log('2. audioManager 상태:', {
exists: !!audioManager,
});

if (audioManager) {
try {
console.log('3. 오디오 스트림 설정 시작');
audioManager.setAudioStream(peerId, stream);
console.log('4. 오디오 스트림 설정 완료');

// 오디오 엘리먼트 생성 확인
setTimeout(() => {
const audioElement = document.getElementById(`audio-${peerId}`);
console.log('5. 생성된 오디오 엘리먼트 확인:', {
exists: !!audioElement,
srcObject: !!(audioElement as HTMLAudioElement)?.srcObject,
volume: (audioElement as HTMLAudioElement)?.volume,
});
}, 100);
} catch (error) {
console.error('오디오 스트림 설정 실패:', error);
}
audioManager.setAudioStream(peerId, stream);
} else {
console.error('audioManager가 설정되지 않음');
}
Expand Down
8 changes: 8 additions & 0 deletions fe/src/stores/zustand/useRoomStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@ interface RoomStore {
rooms: Room[];
currentRoom: Room | null;
currentPlayer: string | null;
kickedPlayer: string | null;
}

interface RoomActions {
setRooms: (rooms: Room[]) => void;
setCurrentRoom: (room: Room) => void;
setCurrentPlayer: (nickname: string) => void;
setKickedPlayer: (nickname: string) => void;
}

const initialState: RoomStore = {
rooms: [],
currentRoom: null,
currentPlayer: null,
kickedPlayer: null,
};

const useRoomStore = create<RoomStore & RoomActions>()(
Expand All @@ -38,6 +41,11 @@ const useRoomStore = create<RoomStore & RoomActions>()(
set(() => ({
currentPlayer: nickname,
})),

setKickedPlayer: (nickname) =>
set(() => ({
kickedPlayer: nickname,
})),
}))
);

Expand Down

0 comments on commit eac257f

Please sign in to comment.