Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FE] main 브랜치에 코드 병합 #185

Merged
merged 11 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions fe/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
VITE_GAME_SERVER_URL="wss://game.example.com/rooms"
VITE_SIGNALING_SERVER_URL="wss://signaling.example.com"
VITE_VOICE_SERVER_URL="wss://voice-processing.example.com"
VITE_STUN_SERVER="stun:coturn.example.com:3478"
VITE_TURN_SERVER="turn:coturn.example.com:3478"
VITE_TURN_USERNAME="your_coturn_username"
VITE_TURN_CREDENTIAL="your_coturn_password"
VITE_GAME_SSE_URL="https://game.example.com/api/rooms/stream"
VITE_GAME_REST_BASE_URL="https://game.example.com/"
17 changes: 15 additions & 2 deletions fe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@

- startGame을 이미 했는데 Intro 화면을 2초 보여줘서 그런 건가 싶음
- 게임 시작 버튼 클릭했을 때 Intro 화면을 GameScreen에서 먼저 2초 띄우고, startGame을 하도록 하면 되려나?
- 순서대로 동작하도록 하는 게 너무 어렵다.
- 원인: Lyric 애니메이션 duration이 timeLimit으로 설정해서, 가사 길이가 짧으면 늦게 등장하게 됐던 것이었다.
- 가사의 길이를 동일하게 맞추지 않는 이상 등장하는 시간을 제한 시간과 완벽하게 맞출 수는 없을 것 같다.
- 해결: Lyric 애니메이션 delay 시간을 -0.5로 설정해서 길이가 짧은 가사는 게임 스크린 중앙쯤부터 등장하도록 했다.
- 적어도 '가사가 왜 안 나오지?' 생각은 안 들 것 같다..!

### 게임 진행 UI 구현 문제: 실시간은 너무 어려워

Expand Down Expand Up @@ -60,7 +63,7 @@ npm install react@latest react-dom@latest
업데이트하고 나서 라우팅 문제 생겨서 다운그레이드함.. 방 나가기 시 나가기 처리가 제대로 안 됨
어떻게 해야 하는지 모르겠다ㅜㅜ

### **VolumeBar 스피커 버튼을 토글하여 볼륨 0 ↔ 50으로 조절할 수 있도록 함**
### VolumeBar 스피커 버튼을 토글하여 볼륨 0 ↔ 50으로 조절할 수 있도록 함

- 진성님이 피드백 주신 부분 반영

Expand All @@ -73,3 +76,13 @@ npm install react@latest react-dom@latest
- Enter로 Submit(확인 버튼 클릭과 동일한 동작)
- shadcn/ui Dialog 컴포넌트는 ESC 키를 눌렀을 때 Dialog Close를 해줘서 이건 따로 처리가 필요 없었다.
- SearchBar(방 검색)에도 적용할 생각!

### 게임 진행 테스트 도중 버그 발견

- 본인 마이크 버튼을 음소거하면 setMute 이벤트를 보내고 updateUsers를 수신해 players 상태를 변경한다.
- 게임 진행 중에 이 마이크 버튼을 음소거하면 각 player의 isMuted 상태가 바뀌고, 이는 currentRoom의 상태를 바꿔 리렌더링 되면서 voice recording이 되지 않는다. (게임방을 나갔을 경우에도 동일, 이 부분은 나중에 해결하기로)
- PlayScreen의 useEffect 의존성 배열에 currentRoom이 있어서 그런 것 같다.
- 그래서 일단 각 player에서 isMuted를 없애고, setMute 시 updateUsers가 아닌 muteStatusChanged 이벤트를 수신해 muteStatus: {닉네임: false/true, ...} 데이터를 받아온다.
- Player 컴포넌트 내부에 isMuted 초기 상태를 정해주고, muteStatus 데이터 상태가 변경되었을 때 isMuted를 변경해 주는 방식으로 바꿨다.
- 이렇게 해서 Player 컴포넌트와 GameScreen 컴포넌트를 독립적으로 리렌더링 해줄 수 있게 됐다.
- 문제: muteStatus의 initial state를 null로 설정하니까 처음에 가져올 때 에러 발생해서 빈 객체로 초기화
Binary file added fe/src/assets/images/angry-pepe.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 45 additions & 0 deletions fe/src/components/common/CustomAlertDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';

interface CustomAlertDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description?: string;
actionText?: string;
}

const CustomAlertDialog = ({
open,
onOpenChange,
title,
description,
actionText = '확인',
}: CustomAlertDialogProps) => {
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent className="font-galmuri sm:max-w-[22rem]">
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
{description && (
<AlertDialogDescription>{description}</AlertDialogDescription>
)}
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction className="bg-primary hover:bg-primary/90">
{actionText}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

export default CustomAlertDialog;
39 changes: 36 additions & 3 deletions fe/src/components/common/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,49 @@
import { Input } from '@/components/ui/input';
import { FiSearch } from 'react-icons/fi';
import { useState, useEffect } from 'react';
import { useDebounce } from '@/hooks/useDebounce';
import useRoomStore from '@/stores/zustand/useRoomStore';
import { searchRoomsQuery } from '@/stores/queries/searchRoomsQuery';
import { getRoomsQuery } from '@/stores/queries/getRoomsQuery';

const SearchBar = () => {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearch = useDebounce(searchTerm, 200); // 200ms 디바운스
const { setRooms } = useRoomStore();
const { data: searchResults } = searchRoomsQuery(debouncedSearch);
const { data: allRooms, refetch: refetchAllRooms } = getRoomsQuery();

// 검색 결과 또는 전체 방 목록으로 업데이트
useEffect(() => {
if (!debouncedSearch.trim()) {
refetchAllRooms();
return;
}

// 검색 결과가 있으면 방 목록 업데이트
if (searchResults) {
setRooms(searchResults);
}
}, [debouncedSearch, searchResults, setRooms, refetchAllRooms]);

// allRooms가 업데이트되면 방 목록 갱신
useEffect(() => {
if (!debouncedSearch.trim() && allRooms) {
setRooms(allRooms);
}
}, [allRooms, debouncedSearch, setRooms]);

return (
<form className="relative w-full mt-6">
<div className="relative w-full mt-6">
<FiSearch className="absolute left-2 top-3 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="검색어를 입력하세요..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="방 제목 검색"
className="font-galmuri pl-8"
/>
</form>
</div>
);
};

Expand Down
125 changes: 125 additions & 0 deletions fe/src/components/game/PitchVisualizer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { motion, AnimatePresence } from 'framer-motion';
import useGameStore from '@/stores/zustand/useGameStore';
import useRoomStore from '@/stores/zustand/useRoomStore';
import usePitchStore from '@/stores/zustand/usePitchStore';
import { signalingSocket } from '@/services/signalingSocket';
import angryPepe from '@/assets/images/angry-pepe.png';
import { PITCH_CONSTANTS } from '@/constants/pitch';
import { usePitchDetection } from '@/hooks/usePitchDetection';

interface PitchVisualizerProps {
isGameplayPhase: boolean;
}

const PitchVisualizer = ({ isGameplayPhase }: PitchVisualizerProps) => {
const { currentPlayer } = useRoomStore();
const { turnData, rank } = useGameStore();
const { currentOpacity, currentVolume, resetPitch } = usePitchStore();

// 클레오파트라 모드 여부와 스트림 상태를 판단하여 피치 검출 훅 호출
const isCleopatraMode = turnData?.gameMode === 'CLEOPATRA';
const isActive = isCleopatraMode && rank.length === 0 && isGameplayPhase;

// 스트림 가져오기
let stream: MediaStream | null = null;
if (isActive) {
if (turnData.playerNickname === currentPlayer) {
stream = signalingSocket.getLocalStream();
} else {
stream = signalingSocket.getPeerStream(turnData.playerNickname);
}
}

// 피치 검출 훅 호출
usePitchDetection(isCleopatraMode && isActive, stream);

// 렌더링 조건 확인
if (!isActive) {
return null;
}

// 볼륨에 따른 스케일 계산
const scale =
PITCH_CONSTANTS.VISUALIZER_MIN_SCALE +
currentVolume * PITCH_CONSTANTS.VISUALIZER_VOLUME_MULTIPLIER;

// 스타일 정의
const containerStyle: React.CSSProperties = {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'none',
zIndex: 9999,
// overflow: 'hidden',
};

const imageContainerStyle: React.CSSProperties = {
position: 'relative',
width: PITCH_CONSTANTS.CONTAINER_SIZE,
height: PITCH_CONSTANTS.CONTAINER_SIZE,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
maxWidth: '100vw',
maxHeight: '100vh',
};

return (
<div style={containerStyle}>
<AnimatePresence>
<motion.div
key="pepe-container"
style={imageContainerStyle}
initial={{
opacity: PITCH_CONSTANTS.MIN_OPACITY,
scale: PITCH_CONSTANTS.VISUALIZER_MIN_SCALE,
}}
animate={{
opacity: currentOpacity * PITCH_CONSTANTS.FREQ_MULTIPLIER,
scale: Math.min(scale, PITCH_CONSTANTS.VISUALIZER_MAX_SCALE),
}}
exit={{
opacity: PITCH_CONSTANTS.MIN_OPACITY,
scale: PITCH_CONSTANTS.VISUALIZER_MIN_SCALE,
}}
transition={{
opacity: {
duration: PITCH_CONSTANTS.OPACITY_TRANSITION_DURATION,
ease: 'linear',
},
scale: {
type: 'spring',
stiffness: 700,
damping: 30,
mass: 1,
duration: PITCH_CONSTANTS.SCALE_TRANSITION_DURATION,
},
}}
>
<img
src={angryPepe}
alt="Angry Pepe"
style={{
width: '100%',
height: '100%',
objectFit: 'contain',
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
maxWidth: '100%',
maxHeight: '100%',
}}
/>
</motion.div>
</AnimatePresence>
</div>
);
};

export default PitchVisualizer;
1 change: 1 addition & 0 deletions fe/src/config/env.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const ENV = {
GAME_SERVER_URL: import.meta.env.VITE_GAME_SERVER_URL,
SIGNALING_SERVER_URL: import.meta.env.VITE_SIGNALING_SERVER_URL,
VOICE_SERVER_URL: import.meta.env.VITE_VOICE_SERVER_URL,
STUN_SERVERS: {
iceServers: [
{
Expand Down
36 changes: 36 additions & 0 deletions fe/src/constants/pitch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export const PITCH_CONSTANTS = {
// 주파수 관련 상수 (Hz 단위)
MIN_FREQ: 100, // 최소 감지 주파수
MAX_FREQ: 1000, // 최대 감지 주파수
MID_FREQ: 550, // 중간 주파수
FREQ_MULTIPLIER: 1.25, // 음계 보정치 배율

// 불투명도 설정
MIN_OPACITY: 0.0, // 최소 불투명도
MAX_OPACITY: 1.0, // 최대 불투명도
INITIAL_OPACITY: 0.0, // 초기 불투명도

// 로깅 및 볼륨 관련
LOG_INTERVAL: 1000, // 로그 출력 간격 (ms)
MIN_VOLUME_THRESHOLD: 0.35, // 최소 인식 볼륨 임계값

// 시각화 관련 상수
VISUALIZER_MIN_SCALE: 0.5, // 최소 스케일
VISUALIZER_MAX_SCALE: 1.25, // 최대 스케일
VISUALIZER_VOLUME_MULTIPLIER: 1.0, // 볼륨에 따른 스케일 배율

// 애니메이션 관련 상수
ANIMATION_SPRING_CONFIG: {
type: 'spring',
stiffness: 700,
damping: 30,
mass: 1,
} as const,

// 트랜지션 타이밍
OPACITY_TRANSITION_DURATION: 0.5, // 불투명도 변화 지속 시간
SCALE_TRANSITION_DURATION: 2.5, // 크기 변화 지속 시간

// 컨테이너 크기 (반응형)
CONTAINER_SIZE: '80vw', // 페페 화면 너비
} as const;
38 changes: 24 additions & 14 deletions fe/src/hooks/useAudioManager.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
import { useCallback } from 'react';

export const useAudioManager = () => {
const setAudioStream = useCallback((peerId: string, stream: MediaStream) => {
const existingAudio = document.getElementById(
`audio-${peerId}`
) as HTMLAudioElement;
const setAudioStream = useCallback(
(
peerId: string,
stream: MediaStream,
playerInfo: {
currentPlayer: string;
isCurrent: boolean;
}
) => {
const existingAudio = document.getElementById(
`audio-${peerId}`
) as HTMLAudioElement;

if (existingAudio) {
existingAudio.remove();
}
if (existingAudio) {
existingAudio.remove();
}

const audioElement = new Audio();
audioElement.id = `audio-${peerId}`;
audioElement.srcObject = stream;
audioElement.autoplay = true;
audioElement.volume = 0.5; // 초기 볼륨
const audioElement = new Audio();
audioElement.id = `audio-${peerId}`;
audioElement.srcObject = stream;
audioElement.autoplay = true;
audioElement.volume = 0.5; // 초기 볼륨

document.body.appendChild(audioElement);
}, []);
document.body.appendChild(audioElement);
},
[]
);

const setVolume = useCallback((peerId: string, volume: number) => {
const audioElement = document.getElementById(
Expand Down
17 changes: 17 additions & 0 deletions fe/src/hooks/useDebounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useState, useEffect } from 'react';

export const useDebounce = <T>(value: T, delay: number): T => {
const [debouncedValue, setDebouncedValue] = useState<T>(value);

useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);

return () => {
clearTimeout(timer);
};
}, [value, delay]);

return debouncedValue;
};
Loading