Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
kwaksj329 committed Nov 28, 2024
2 parents dca2082 + 8bbfa91 commit 7814a5f
Show file tree
Hide file tree
Showing 20 changed files with 447 additions and 36 deletions.
3 changes: 2 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
"build-storybook": "storybook build"
},
"dependencies": {
"@troublepainter/core": "workspace:*",
"@lottiefiles/dotlottie-react": "^0.10.1",
"@tanstack/react-query": "^5.59.19",
"@troublepainter/core": "workspace:*",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"react": "^18.3.1",
Expand Down
2 changes: 1 addition & 1 deletion client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ToastContainer } from './components/toast/ToastContainer';
import { ToastContainer } from '@/components/toast/ToastContainer';

// React Query 클라이언트 인스턴스 생성
const queryClient = new QueryClient({
Expand Down
9 changes: 7 additions & 2 deletions client/src/api/api.config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// 서버 URL
const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000';
const PRODUCTION_URL = 'www.troublepainter.site';

// const SOCKET_URL = import.meta.env.VITE_SOCKET_URL || 'http://localhost:3000';

export const API_CONFIG = {
BASE_URL,
ENDPOINTS: {
GAME: {
CREATE_ROOM: '/api/game/rooms',
CREATE_ROOM: '/game/rooms',
},
},
OPTIONS: {
Expand Down Expand Up @@ -92,7 +94,10 @@ export class ApiError extends Error {
* };
*/
export async function fetchApi<T>(endpoint: string, options?: RequestInit): Promise<T> {
const response = await fetch(`${endpoint}`, {
const isProductionHost = window.location.origin.includes(PRODUCTION_URL);
const url = isProductionHost ? `/api${endpoint}` : `${BASE_URL}${endpoint}`;

const response = await fetch(url, {
...API_CONFIG.OPTIONS,
...options,
headers: {
Expand Down
Binary file added client/src/assets/lottie/game-win.lottie
Binary file not shown.
Binary file added client/src/assets/lottie/loading.lottie
Binary file not shown.
Binary file added client/src/assets/lottie/round-loss.lottie
Binary file not shown.
Binary file added client/src/assets/lottie/round-win.lottie
Binary file not shown.
30 changes: 30 additions & 0 deletions client/src/assets/sound-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/src/assets/sounds/background-music.mp3
Binary file not shown.
Binary file added client/src/assets/sounds/entry-sound-effect.mp3
Binary file not shown.
52 changes: 52 additions & 0 deletions client/src/components/bgm-button/BackgroundMusicButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useState } from 'react';
import soundLogo from '@/assets/sound-logo.svg';
import { useBackgroundMusic } from '@/hooks/useBackgroundMusic';
import { cn } from '@/utils/cn';

export const BackgroundMusicButton = () => {
const { volume, togglePlay, adjustVolume } = useBackgroundMusic();
const [isHovered, setIsHovered] = useState(false);

const isMuted = volume === 0;

return (
<div
className="fixed left-4 top-4 z-30 flex flex-col items-center gap-2 xs:left-8 xs:top-8"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* 음소거/재생 토글 버튼 */}
<button
onClick={togglePlay}
className={cn(
'flex h-10 w-10 items-center justify-center rounded-full bg-chartreuseyellow-600 text-white transition-colors hover:bg-chartreuseyellow-500',
isMuted && 'opacity-50 grayscale',
)}
aria-label={isMuted ? '배경음악 재생' : '배경음악 음소거'}
>
<img src={soundLogo} className="h-8 w-8 transition-all duration-300" />
</button>

{/* 볼륨 슬라이더 */}
<div
className={cn(
'flex flex-col items-center rounded-lg bg-chartreuseyellow-500 p-2 transition-all duration-300',
isHovered ? 'translate-y-0 opacity-100' : 'pointer-events-none -translate-y-4 opacity-0',
)}
>
<input
type="range"
min="0"
max="1"
step="0.1"
value={volume}
onChange={(e) => adjustVolume(Number(e.target.value))}
className="h-24 w-1 appearance-none rounded-full bg-chartreuseyellow-200 [-webkit-appearance:slider-vertical] [writing-mode:bt-lr]"
aria-label="배경음악 볼륨 조절"
/>
</div>
</div>
);
};

export default BackgroundMusicButton;
111 changes: 84 additions & 27 deletions client/src/components/modal/RoundEndModal.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,104 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { DotLottieReact } from '@lottiefiles/dotlottie-react';
import { PlayerRole } from '@troublepainter/core';
import { Modal } from '../ui/Modal';
import roundLoss from '@/assets/lottie/round-loss.lottie';
import roundWin from '@/assets/lottie/round-win.lottie';
import { Modal } from '@/components/ui/Modal';
import { useModal } from '@/hooks/useModal';
import { useGameSocketStore } from '@/stores/socket/gameSocket.store';
import { cn } from '@/utils/cn';

const RoundEndModal = () => {
const { room, roundWinner, players, timers } = useGameSocketStore();
const { room, roundWinner, players, timers, currentPlayerId } = useGameSocketStore();
const { isModalOpened, openModal, closeModal } = useModal();
const [showAnimation, setShowAnimation] = useState(true);
const [isAnimationFading, setIsAnimationFading] = useState(false);

useEffect(() => {
if (roundWinner) openModal();
if (roundWinner) {
setIsAnimationFading(false);
setShowAnimation(true);
openModal();
}
}, [roundWinner]);

useEffect(() => {
if (timers.ENDING === 0) closeModal();
}, [timers.ENDING]);

const devil = players.find((player) => player.role === PlayerRole.DEVIL);
const isDevilWin = roundWinner?.role === PlayerRole.DEVIL;
const isCurrentPlayerWinner = currentPlayerId === roundWinner?.playerId;

useEffect(() => {
if (showAnimation) {
// 3초 후에 페이드아웃 시작
const fadeTimer = setTimeout(() => {
setIsAnimationFading(true);
}, 3000);

// 3.5초 후에 컴포넌트 제거
const removeTimer = setTimeout(() => {
setShowAnimation(false);
}, 3500);

return () => {
clearTimeout(fadeTimer);
clearTimeout(removeTimer);
};
}
}, [showAnimation]);

return (
<Modal
title={room?.currentWord || ''}
isModalOpened={isModalOpened}
className="max-w-[26.875rem] sm:max-w-[61.75rem]"
>
<div className="flex min-h-[12rem] items-center justify-center sm:min-h-[15.75rem]">
<p className="text-center text-2xl sm:m-2 sm:text-3xl">
{roundWinner?.role === PlayerRole.DEVIL ? (
<> 정답을 맞춘 구경꾼이 없습니다</>
) : (
<>
구경꾼 <span className="text-violet-600">{roundWinner?.nickname}</span>이 정답을 맞혔습니다
</>
)}
</p>
</div>
<div className="min-h-[4rem] rounded-md bg-violet-50 p-4 sm:m-2">
<p className="text-center text-xl text-violet-950 sm:text-2xl">
방해꾼은 <span className="text-violet-600">{devil?.nickname}</span>였습니다.
</p>
<span>{timers.ENDING}</span> {/* 임시 */}
</div>
</Modal>
<>
{/* 승리/패배 애니메이션 */}
{showAnimation &&
(isCurrentPlayerWinner ? (
<DotLottieReact
src={roundWin}
autoplay
loop={false}
className={cn(
'absolute left-1/2 top-1/2 z-50 h-screen w-full -translate-x-1/2 -translate-y-1/2 transition-opacity duration-500',
isAnimationFading && 'opacity-0',
)}
/>
) : (
<DotLottieReact
src={roundLoss}
autoplay
loop={false}
className={cn(
'absolute left-1/2 top-1/2 z-50 h-[50vh] w-full -translate-x-1/2 -translate-y-1/2 transition-opacity duration-500',
isAnimationFading && 'opacity-0',
)}
/>
))}

<Modal
title={room?.currentWord || ''}
isModalOpened={isModalOpened}
className="max-w-[26.875rem] sm:max-w-[61.75rem]"
>
<div className="flex min-h-[12rem] items-center justify-center sm:min-h-[15.75rem]">
<p className="text-center text-2xl sm:m-2 sm:text-3xl">
{isDevilWin ? (
<> 정답을 맞춘 구경꾼이 없습니다</>
) : (
<>
구경꾼 <span className="text-violet-600">{roundWinner?.nickname}</span>이 정답을 맞혔습니다
</>
)}
</p>
</div>
<div className="min-h-[4rem] rounded-md bg-violet-50 p-4 sm:m-2">
<p className="text-center text-xl text-violet-950 sm:text-2xl">
방해꾼은 <span className="text-violet-600">{devil?.nickname}</span>였습니다.
</p>
<span>{timers.ENDING}</span>
</div>
</Modal>
</>
);
};

Expand Down
12 changes: 12 additions & 0 deletions client/src/hooks/socket/useGameSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import {
TimerType,
} from '@troublepainter/core';
import { useNavigate, useParams } from 'react-router-dom';
import entrySound from '@/assets/sounds/entry-sound-effect.mp3';
import { gameSocketHandlers } from '@/handlers/socket/gameSocket.handler';
import { useGameSocketStore } from '@/stores/socket/gameSocket.store';
import { SocketNamespace } from '@/stores/socket/socket.config';
import { useSocketStore } from '@/stores/socket/socket.store';
import { checkTimerDifference } from '@/utils/checkTimerDifference';
import { playerIdStorageUtils } from '@/utils/playerIdStorage';
import { SOUND_IDS, SoundManager } from '@/utils/soundManager';

/**
* 게임 진행에 필요한 소켓 연결과 상태를 관리하는 Hook입니다.
Expand Down Expand Up @@ -107,10 +109,18 @@ export const useGameSocket = () => {
};
}, [roomId]);

// 컴포넌트 마운트 시 사운드 미리 로드
useEffect(() => {
const soundManager = SoundManager.getInstance();
soundManager.preloadSound(SOUND_IDS.ENTRY, entrySound);
}, []);

useEffect(() => {
const socket = sockets.game;
if (!socket || !roomId) return;

const soundManager = SoundManager.getInstance();

const handlers = {
joinedRoom: (response: JoinRoomResponse) => {
const { room, roomSettings, players, playerId } = response;
Expand All @@ -121,6 +131,7 @@ export const useGameSocket = () => {
playerIdStorageUtils.setPlayerId(roomId, playerId);
gameActions.updateCurrentPlayerId(playerId);
gameActions.updateIsHost(room.hostId === playerId);
void soundManager.playSound(SOUND_IDS.ENTRY, 0.5);
}
},

Expand All @@ -129,6 +140,7 @@ export const useGameSocket = () => {
gameActions.updateRoom(room);
gameActions.updateRoomSettings({ ...roomSettings, drawTime: roomSettings.drawTime - 5 });
gameActions.updatePlayers(players);
void soundManager.playSound(SOUND_IDS.ENTRY, 0.5);
},

playerLeft: (response: PlayerLeftResponse) => {
Expand Down
Loading

0 comments on commit 7814a5f

Please sign in to comment.