diff --git a/app/(nav)/home/page.tsx b/app/(nav)/home/page.tsx index 247e9e5..9cd93d0 100644 --- a/app/(nav)/home/page.tsx +++ b/app/(nav)/home/page.tsx @@ -37,6 +37,68 @@ const handleRegularNotification = (notification: NotificationMessage) => { } ); }; +// notification: NotificationMessage, +// router: AppRouterInstance, +// addMessageFn: (message: any) => void +// ) => { +// toast.custom( +// (t) => ( +// { +// const stompClient = new Client({ +// brokerURL: `${process.env.NEXT_PUBLIC_GROUP_WS_URL}/group-service/connect`, +// }); + +// // 연결 시도 전에 이벤트 핸들러 설정 +// stompClient.onConnect = () => { +// console.log('WebSocket 연결 성공'); + +// // 구독 설정 +// const subscription = stompClient.subscribe( +// `${process.env.NEXT_PUBLIC_GROUP_SUBSCRIBE}/${notification.groupId}`, +// (message: { body: string }) => { +// console.log('Received message:', message.body); +// const parsedMessage = JSON.parse(message.body); +// addMessageFn({ +// ...parsedMessage, +// timestamp: Date.now(), +// }); +// } +// ); +// console.log('구독 설정 완료'); +// }; + +// stompClient.onConnect = () => { +// stompClient.subscribe( +// `${process.env.NEXT_PUBLIC_GROUP_SUBSCRIBE}/${notification.groupId}`, +// (message: { body: string }) => { +// console.log('Received message:', message.body); +// const parsedMessage = JSON.parse(message.body); +// addMessageFn({ +// ...parsedMessage, +// timestamp: Date.now(), +// }); +// } +// ); +// }; +// stompClient.activate(); +// router.push(`/game/${notification.groupId}/waitingRoom`); +// toast.dismiss(t.id); +// }} +// onReject={() => { +// toast.dismiss(t.id); +// }} +// toastId={t.id} +// /> +// ), +// { +// position: 'top-center', +// duration: 30000, +// } +// ); +// }; +// ... existing code ... const handleInvitation = ( notification: NotificationMessage, @@ -72,6 +134,9 @@ const handleInvitation = ( const connectSSE = (userId: string) => { const { setEventSource } = useSSEStore.getState(); + // let retryCount = 0; + // const MAX_RETRIES = 3; + // let retryTimeout: NodeJS.Timeout; try { const url = `/api/notification-service/connect/${encodeURIComponent(userId)}`; diff --git a/app/game/[roomId]/[round]/layout.tsx b/app/game/[roomId]/[round]/layout.tsx index 12a54eb..07b99e3 100644 --- a/app/game/[roomId]/[round]/layout.tsx +++ b/app/game/[roomId]/[round]/layout.tsx @@ -2,8 +2,8 @@ import { useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; -import { motion } from 'framer-motion'; -import colors from '@/styles/color/palette'; +import SoundOn from '@/styles/Icon/SoundOn.svg'; +import SoundOff from '@/styles/Icon/SoundOff.svg'; const InGameLayout = ({ children }: { children: React.ReactNode }) => { const audioRef = useRef(null); @@ -91,12 +91,7 @@ const MusicSwitch = ({ onClick(); }} > - - - + {isOn ? : } ); }; @@ -108,7 +103,7 @@ const Container = styled.div` const SwitchContainer = styled.div` position: absolute; top: 0.5rem; - right: 0.5rem; + left: 1rem; z-index: 50; cursor: pointer; margin: 0 auto; @@ -119,23 +114,20 @@ const SwitchContainer = styled.div` } `; -const SwitchTrack = styled(motion.div)<{ isOn: boolean }>` - width: 50px; +const SoundOnIcon = styled.img.attrs({ + src: SoundOn.src, + alt: 'Sound On', +})` + width: 24px; height: 24px; - background-color: ${({ isOn }) => - isOn ? colors.purple[60] : colors.gray[80]}; - border-radius: 12px; - display: flex; - align-items: center; - justify-content: ${({ isOn }) => (isOn ? 'flex-end' : 'flex-start')}; - padding: 2px; `; -const SwitchThumb = styled(motion.div)` - width: 20px; - height: 20px; - background-color: white; - border-radius: 50%; +const SoundOffIcon = styled.img.attrs({ + src: SoundOff.src, + alt: 'Sound Off', +})` + width: 24px; + height: 24px; `; export default InGameLayout; diff --git a/app/game/[roomId]/[round]/page.tsx b/app/game/[roomId]/[round]/page.tsx index 48ef706..60384d6 100644 --- a/app/game/[roomId]/[round]/page.tsx +++ b/app/game/[roomId]/[round]/page.tsx @@ -1,18 +1,16 @@ 'use client'; import { useRouter } from 'next/navigation'; import { useGameState } from '@/hooks/inGame/useGameState'; - -import useUserStore from '@/stores/useUserStore'; +// import useUserStore from '@/stores/useUserStore'; import TopBar from '@/components/Common/TopBar/TopBar'; import Button from '@/components/Common/Button/Button'; import Timer from '@/components/Layout/Game/Timer'; - import MapBottomSheet from '@/components/Layout/Game/MapBottomSheet'; - import SwiperComponent from '@/components/Layout/Game/Swiper'; import MapComponent from '@/components/Layout/Game/GoogleMap'; import { useCallback, useState, useEffect } from 'react'; -import { useWebSocket } from '@/hooks/inGame/useWebSocket'; +import InGameWebSocket from '@/lib/websocket/gameWebsocket'; +import useWebSocketStore, { RoundData } from '@/stores/useWebSocketStore'; import { PageWrapper, @@ -31,28 +29,43 @@ interface GamePageProps { const GamePage = ({ params }: GamePageProps) => { const router = useRouter(); - const nickname = useUserStore( - (state: { nickname: string | null }) => state.nickname - ); - - // const { submitAnswer, isSubmitting } = useGameSubmit(); + // const nickname = useUserStore((state) => state.nickname); const [hasSubmitted, setHasSubmitted] = useState(false); - // WebSocket - const { gameState, submitAnswer, createRound } = useWebSocket( - Number(params.roomId) - ); - const { + gameState, currentRound, isMapView, showBackIcon, currentSelectedCoordinate, - roundState, setCurrentSelectedCoordinate, handleBackClick, } = useGameState(Number(params.round)); + const messages = useWebSocketStore((state) => state.messages); + + // roundState를 별도의 state로 관리 + const [roundState, setRoundState] = useState(null); + + // messages와 currentRound 변경 시 roundState 업데이트 + useEffect(() => { + const roundStartMessage = messages + .filter((msg) => msg.eventType === 'ROUND_START') + .find((msg) => (msg.data as RoundData).roundNum === currentRound); + + if (roundStartMessage) { + setRoundState(roundStartMessage.data as RoundData); + console.log( + 'Round state updated for round', + currentRound, + ':', + roundStartMessage.data + ); + } + }, [messages, currentRound]); + + const webSocket = InGameWebSocket.getInstance(); + const handleNextRound = useCallback(() => { router.push(`/game/${params.roomId}/${currentRound}/roundRank`); setCurrentSelectedCoordinate(null); @@ -65,78 +78,20 @@ const GamePage = ({ params }: GamePageProps) => { setCurrentSelectedCoordinate(coordinate); }; - // TODO: 백엔드 연동 시 사용 - // const handleSubmitAnswer = async () => { - // if (hasSubmitted || !currentSelectedCoordinate) { - // console.log('제출 불가:', { hasSubmitted, currentSelectedCoordinate }); - // return; - // } - - // const submitData = { - // nickname: nickname || '', - // roundId: Number(params.round), - // coordinate: [ - // currentSelectedCoordinate.lat, - // currentSelectedCoordinate.lng, - // ], - // }; - - // // 제출 시작과 동시에 버튼 비활성화 - // setHasSubmitted(true); - // console.log('제출 시작:', submitData); - - // try { - // const success = await submitAnswer(submitData); - // console.log('제출 결과:', success); - - // if (!success) { - // console.warn('제출 실패'); - // setHasSubmitted(false); // 실패시에만 다시 활성화 - // return; - // } - - // setCurrentSelectedCoordinate(null); - // console.log('제출 완료'); - // } catch (error) { - // console.error('제출 중 에러 발생:', error); - // setHasSubmitted(false); // 에러 발생시에도 다시 활성화 - // } - // }; - - // 클라이언트 테스트 용 - + // 답안 제출 함수 수정 const handleSubmitAnswer = async () => { if (hasSubmitted || !currentSelectedCoordinate) { console.log('제출 불가:', { hasSubmitted, currentSelectedCoordinate }); return; } - const submitData = { - nickname: nickname || '', - roundId: Number(params.round), - coordinate: [ - currentSelectedCoordinate.lat, - currentSelectedCoordinate.lng, - ] as [number, number], - }; - setHasSubmitted(true); - // API 호출 대신 setTimeout으로 테스트 try { - await new Promise((resolve) => setTimeout(resolve, 1000)); // 1초 딜레이 - const mockSuccess = true; // 테스트용 성공 응답 - - console.log('제출 결과:', mockSuccess); - - if (!mockSuccess) { - console.warn('제출 실패'); - setHasSubmitted(false); - return; - } - - submitAnswer(submitData); - + webSocket.submitAnswer(Number(params.round), [ + currentSelectedCoordinate.lat, + currentSelectedCoordinate.lng, + ]); setCurrentSelectedCoordinate(null); console.log('제출 완료'); } catch (error) { @@ -156,19 +111,9 @@ const GamePage = ({ params }: GamePageProps) => { } }; - useEffect(() => { - // 라운드 시작시 라운드 생성 요청 - createRound(Number(params.round)); - }, [createRound, params.round]); - - // gameState에서 라운드 정보 활용 - useEffect(() => { - if (gameState.roundState) { - // 라운드 상태 업데이트시 처리 - console.log('새로운 라운드 정보:', gameState.roundState); - } - }, [gameState.roundState]); - + // 디버깅용 콘솔 로그 추가 + console.log('현재 gameState:', gameState); + console.log('contentUrls:', gameState.roundState?.contentUrls); return ( <> @@ -184,13 +129,6 @@ const GamePage = ({ params }: GamePageProps) => { {/* 기본 뷰 (스와이퍼와 힌트) */} - - {roundState.hint && ( - - 힌트 - {roundState.hint} - - )} {isMapView ? ( { /> ) : ( <> - - {/* TODO: 백엔드 연동 시 사용 */} - {/* */} - {roundState.hint && ( + + {roundState?.hint && ( 힌트 {roundState.hint} - {/* {gameState.roundState.hint} */} )} diff --git a/app/game/[roomId]/[round]/roundRank/page.tsx b/app/game/[roomId]/[round]/roundRank/page.tsx index 48713ba..f6c9f8b 100644 --- a/app/game/[roomId]/[round]/roundRank/page.tsx +++ b/app/game/[roomId]/[round]/roundRank/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useMemo, useState, useCallback } from 'react'; +import { useState, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import TopBar from '@/components/Common/TopBar/TopBar'; import Timer from '@/components/Layout/Game/Timer'; @@ -8,187 +8,92 @@ import { PageWrapper, Footer } from '../game.styles'; import RankList from '@/components/Layout/Game/RankList'; import MapComponent from '@/components/Layout/Game/GoogleMap'; import CountdownButton from '@/components/Layout/Game/CountdownButton'; -import { useWebSocket } from '@/hooks/inGame/useWebSocket'; +import InGameWebSocket from '@/lib/websocket/gameWebsocket'; +import useWebSocketStore from '@/stores/useWebSocketStore'; + +interface Coordinate { + nickname: string; + lat: number; + lng: number; + score: number; + totalScore: number; +} + +interface ScoreDataType { + answerCoordinate: { + lat: number; + lng: number; + }; + submitCoordinates: Coordinate[]; +} const RoundRank = ({ params, }: { params: { round: string; roomId: string }; }) => { - const { gameState } = useWebSocket(Number(params.roomId)); // TODO: 라운드 데이터 받아오기 const router = useRouter(); - const currentRound = Number(params.round) || 1; // 기본값 설정 - const maxRounds = 3; // TODO : prop으로 라운드 받아오기 - - const MapDummyData = useMemo(() => { - // TODO: 나중에 데이터를 가져와서 쓸땐 useMemo쓰기 - return { - answerCoordinate: { lat: 37.5665, lng: 126.958 }, - userCoordinates: [ - { nickname: '가가가', lat: 37.5665, lng: 126.978, score: 120 }, - { nickname: '나나나', lat: 37.5675, lng: 126.979, score: 110 }, - { nickname: '다다다', lat: 37.5685, lng: 126.98, score: 100 }, - { nickname: '라라라', lat: 37.5695, lng: 126.981, score: 90 }, - { nickname: '마마마', lat: 37.5705, lng: 126.982, score: 80 }, - { nickname: '바바바', lat: 37.5715, lng: 126.983, score: 70 }, - ], - }; - }, []); - - // TODO: ws 연동 시 사용 - // // 이번 라운드 점수 기준 정렬 - // const thisRoundRankData = useMemo(() => { - // if (!gameState.scoreState?.submitCoordinates) return []; - - // return [...gameState.scoreState.submitCoordinates] - // .sort((a, b) => b.score - a.score) // 점수 내림차순 정렬 - // .map((coord, index) => ({ - // rank: index + 1, - // name: coord.nickname, - // score: coord.score, - // totalScore: coord.totalScore - // })); - // }, [gameState.scoreState]); - - // // 누적 점수 기준 정렬 - // const totalRoundRankData = useMemo(() => { - // if (!gameState.scoreState?.submitCoordinates) return []; - - // return [...gameState.scoreState.submitCoordinates] - // .sort((a, b) => b.totalScore - a.totalScore) // 총점 내림차순 정렬 - // .map((coord, index) => ({ - // rank: index + 1, - // name: coord.nickname, - // score: coord.score, - // totalScore: coord.totalScore - // })); - // }, [gameState.scoreState]); - - // // 동점자 처리 로직 - // const assignRankWithTies = (sortedData: any[]) => { - // let currentRank = 1; - // let currentScore = -1; - // let sameRankCount = 0; - - // return sortedData.map((item, index) => { - // const score = currentRoundData === 'thisRound' ? item.score : item.totalScore; - - // if (score !== currentScore) { - // currentRank = index + 1; - // currentScore = score; - // sameRankCount = 0; - // } else { - // sameRankCount++; - // } - - // return { - // ...item, - // rank: currentRank - // }; - // }); - // }; - - // // 현재 보여줄 데이터 선택 - // const displayRankData = useMemo(() => { - // const rawData = currentRoundData === 'thisRound' - // ? thisRoundRankData - // : totalRoundRankData; - - // return assignRankWithTies(rawData); - // }, [currentRoundData, thisRoundRankData, totalRoundRankData]); - - // 상태로 현재 라운드 선택 관리 + const currentRound = Number(params.round) || 1; + const maxRounds = 3; + const messages = useWebSocketStore((state) => state.messages); const [currentRoundData, setCurrentRoundData] = useState< 'thisRound' | 'totalRound' >(currentRound >= maxRounds ? 'totalRound' : 'thisRound'); + const resultMessage = messages[messages.length - 1]; + // console.log('All messages:', messages); + // console.log('Result message:', resultMessage); + const scoreData = resultMessage?.data as ScoreDataType; + // console.log('Score data:', scoreData); + + const rankData = !scoreData?.submitCoordinates + ? [] + : scoreData.submitCoordinates + .sort((a, b) => + currentRoundData === 'thisRound' + ? b.score - a.score + : b.totalScore - a.totalScore + ) + .map((coord) => ({ + rank: 0, + name: coord.nickname, + score: + currentRoundData === 'thisRound' ? coord.score : coord.totalScore, + totalScore: coord.totalScore, + })); + const handleToggle = (round: 'thisRound' | 'totalRound') => { setCurrentRoundData(round); }; - // 이번 라운드 계산 (useMemo로 렌더링 한번만) - const thisRoundData = useMemo( - () => - MapDummyData.userCoordinates.map((user, index) => ({ - rank: index + 1, - name: user.nickname, - score: user.score, - })), - [MapDummyData] - ); - - const totalRoundData = useMemo( - () => - MapDummyData.userCoordinates.map((user, index) => ({ - rank: index + 1, - name: user.nickname, - score: user.score * currentRound, - })), - [MapDummyData, currentRound] - ); - - const dummyData = useMemo( - () => ({ - thisRound: thisRoundData, - totalRound: totalRoundData, - }), - [thisRoundData, totalRoundData] - ); - - // 다음 라운드로 이동하거나 /home으로 리디렉션 const handleNextRound = useCallback(() => { if (currentRound >= maxRounds) { + const webSocket = InGameWebSocket.getInstance(); + webSocket.disconnect(); router.push('/home'); } else { router.push(`/game/${params.roomId}/${currentRound + 1}`); } - }, [router, params.roomId, currentRound]); - - // TODO: ws 연동 시 사용 - // gameState.scoreState를 사용하여 실제 데이터 활용 - // const rankData = useMemo(() => { - // if (!gameState.scoreState) return []; - - // return gameState.scoreState.submitCoordinates.map((coord, index) => ({ - // rank: index + 1, - // name: coord.nickname, - // score: coord.score, - // totalScore: coord.totalScore - // })); - // }, [gameState.scoreState]); + }, [router, params.roomId, currentRound, maxRounds]); return ( - {/* 타이머와 게이지 */} {currentRound < maxRounds && ( )} - {/* TODO: ws 연동 시 사용 */} - {/* */} = maxRounds ? 'totalRound' : 'thisRound' } /> - {/* TODO: ws 연동 시 사용 */} - {/* = maxRounds ? 'totalRound' : 'thisRound'} - /> */} - {/* 버튼: 마지막 라운드나 3라운드일 경우 '최종 점수 확인' */} {currentRound >= maxRounds && (