diff --git a/src/pages/BattleAnimation.tsx b/src/components/BattleAnimation.tsx similarity index 100% rename from src/pages/BattleAnimation.tsx rename to src/components/BattleAnimation.tsx diff --git a/src/components/RpsGame.tsx b/src/components/RpsGame.tsx new file mode 100644 index 0000000..bb19139 --- /dev/null +++ b/src/components/RpsGame.tsx @@ -0,0 +1,183 @@ +import { useEffect, useState } from "react"; +import SockJS from "sockjs-client"; +import Stomp from "stompjs"; +import EventeeButton from "./EventeeButton"; + +import { + RPSType, + PlayerDto, + StartGameDto, + GamePlayDto, +} from "./../types/game/rps"; + +interface Props { + eventId: number; + apiUrl: string; + myMemberId: number; + myNickname: string; +} + +export default function RpsGame({ + eventId, + apiUrl, + myMemberId, + myNickname, +}: Props) { + const [client, setClient] = useState(null); + + const [started, setStarted] = useState(false); + const [leader, setLeader] = useState(null); + const [players, setPlayers] = useState([]); + const [gameId, setGameId] = useState(null); + + const [myChoice, setMyChoice] = useState(null); + const [isAlive, setIsAlive] = useState(true); + + /* =========================== + WebSocket 연결 + =========================== */ + useEffect(() => { + const socket = new SockJS(`${apiUrl}/ws`); + const stomp = Stomp.over(socket); + + stomp.connect({}, () => { + // 게임 시작 (목록용) + stomp.subscribe(`/sub/game/${eventId}/start`, (msg) => { + const data: StartGameDto = JSON.parse(msg.body); + + setStarted(true); + setLeader(data.leader); + setPlayers(data.players); + setGameId(null); + setMyChoice(null); + setIsAlive(true); + }); + + // 라운드 결과 + stomp.subscribe(`/sub/game/${eventId}/round`, (msg) => { + const data: GamePlayDto = JSON.parse(msg.body); + + setLeader(data.leader); + setPlayers(data.players); + setGameId(data.gameId); + + const me = data.players.find( + (p) => p.memberId === myMemberId + ); + setIsAlive(!!me); + }); + }); + + setClient(stomp); + return () => { + if (client) { + try { + client.disconnect(() => {}); + } catch (e) { + console.warn('[EventMainPage] client.disconnect error', e); + } + } + }; + }, [eventId]); + + /* =========================== + 선택 전송 + =========================== */ + const play = (type: RPSType) => { + if (!client || !gameId) return; + + setMyChoice(type); + + client.send( + `/pub/game/${eventId}/play`, + {}, + JSON.stringify({ + memberId: myMemberId, + nickname: myNickname, + type, + }) + ); + }; + + /* =========================== + 게임 시작 (사회자) + =========================== */ + const startGame = () => { + if (!client) return; + + client.send( + `/pub/game/${eventId}/start`, + {}, + JSON.stringify({ eventId }) + ); + }; + + /* =========================== + UI + =========================== */ + return ( +
+

✊✌️✋ 가위바위보

+ + {!started && ( + + 가위바위보 시작 + + )} + + {started && ( + <> +

+ 🎤 사회자: {leader?.nickname} +

+ +
+ 생존자: +
    + {players.map((p) => ( +
  • {p.nickname}
  • + ))} +
+
+ + )} + + {/* 선택 UI */} + {gameId && isAlive && ( +
+ + + +
+ )} + + {!isAlive && gameId && ( +

+ ❌ 탈락 +

+ )} +
+ ); +} + +/* =========================== + 버튼 컴포넌트 +=========================== */ +function RpsButton({ + label, + type, + onClick, +}: { + label: string; + type: RPSType; + onClick: (t: RPSType) => void; +}) { + return ( + + ); +} diff --git a/src/pages/EventMainPage.tsx b/src/pages/EventMainPage.tsx index cb64d67..484a9bb 100644 --- a/src/pages/EventMainPage.tsx +++ b/src/pages/EventMainPage.tsx @@ -22,7 +22,8 @@ import { apiFetch } from "../utils/apiFetch"; import axios from 'axios'; import SockJS from 'sockjs-client'; import Stomp from 'stompjs'; -import Battle from "./BattleAnimation"; +import Battle from "../components/BattleAnimation"; +import RpsGame from "../components/RpsGame"; type Comment = { id: string; @@ -97,6 +98,10 @@ export default function EventMainPage() { const { user } = useApp(); const [stompClient, setStompClient] = useState(null); + if (!user) { + return null; + } + const API_URL = import.meta.env.VITE_API_URL; const eventTitle = location.state?.eventTitle || "이벤트"; @@ -1604,7 +1609,19 @@ export default function EventMainPage() { fire={fire} explode={explode} result={result} - /> + /> +
+ + 대포쏘기 + + + +
); } diff --git a/src/types/game/rps.ts b/src/types/game/rps.ts new file mode 100644 index 0000000..2b7c5ad --- /dev/null +++ b/src/types/game/rps.ts @@ -0,0 +1,24 @@ +export type RPSType = "ROCK" | "PAPER" | "SCISSOR"; + +// 서버 PlayerDto +export interface PlayerDto { + memberId: number; + nickname: string; + type?: RPSType; // start 단계에는 없음 +} + +// /sub/game/{eventId}/start +export interface StartGameDto { + eventId: number; + winnerCnt: number; + leader: PlayerDto; + players: PlayerDto[]; +} + +// /sub/game/{eventId}/round +export interface GamePlayDto { + leader: PlayerDto; + players: PlayerDto[]; + gameId: number; + eventId: number; +} \ No newline at end of file