diff --git a/fe/src/components/GameBoard/Cell.tsx b/fe/src/components/GameBoard/Cell.tsx index 539fbf4..d6ed76d 100644 --- a/fe/src/components/GameBoard/Cell.tsx +++ b/fe/src/components/GameBoard/Cell.tsx @@ -43,11 +43,11 @@ export default function Cell({ > {cell.theme && (
- {cell.name}
)} + {cell.theme && } {!cell.theme && } {price && {addCommasToNumber(price)}} @@ -71,8 +71,10 @@ const Container = styled.div<{ `; const Header = styled.div` + min-height: 2rem; display: flex; justify-content: center; + align-items: center; background-color: ${({ theme: { color } }) => color.accentText}; `; @@ -94,6 +96,6 @@ const Content = styled.div` height: 100%; display: flex; flex-direction: column; - justify-content: center; + justify-content: space-evenly; align-items: center; `; diff --git a/fe/src/components/GameBoard/GameBoard.tsx b/fe/src/components/GameBoard/GameBoard.tsx index d146931..4d96f8b 100644 --- a/fe/src/components/GameBoard/GameBoard.tsx +++ b/fe/src/components/GameBoard/GameBoard.tsx @@ -1,3 +1,4 @@ +import useGetSocketUrl from '@hooks/useGetSocketUrl'; import { playerAtomsAtom, useGameInfoValue, @@ -6,6 +7,8 @@ import { } from '@store/reducer'; import { useAtom } from 'jotai'; import { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import useWebSocket from 'react-use-websocket'; import { css, styled } from 'styled-components'; import Cell from './Cell'; import CenterArea from './CenterArea'; @@ -18,12 +21,28 @@ export default function GameBoard() { const stockList = useStocksValue(); const players = usePlayersValue(); const [playerAtoms] = useAtom(playerAtomsAtom); + const socketUrl = useGetSocketUrl(); + const { gameId } = useParams(); + const { sendJsonMessage } = useWebSocket(socketUrl, { + share: true, + }); + const isEveryoneReady = players.every( + (player) => player.playerId === '' || player.isReady + ); const currentPlayer = players.find( (player) => player.playerId === gameInfo.currentPlayerId ); const currentPlayerStatus = currentPlayer?.gameboard.status ?? 'event'; + const handleStart = () => { + const message = { + type: 'start', + gameId, + }; + sendJsonMessage(message); + }; + const selectTargetLocation = (location: number) => { setTargetLocation(location); }; @@ -33,7 +52,7 @@ export default function GameBoard() { }; return ( - <> + {initialBoard.map((line, index) => ( @@ -54,6 +73,9 @@ export default function GameBoard() { })} ))} + {!gameInfo.isPlaying && isEveryoneReady && ( + + )} {gameInfo.isPlaying && ( ; })} - + ); } +const Container = styled.div` + position: relative; + display: flex; + justify-content: center; + align-items: center; +`; + const Board = styled.div` + width: 42rem; + height: 42rem; min-width: 42rem; min-height: 42rem; position: relative; @@ -82,6 +113,18 @@ const Line = styled.div<{ $lineNum: number }>` ${({ $lineNum }) => drawLine($lineNum)} `; +const Button = styled.button` + width: 6rem; + height: 4rem; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border-radius: ${({ theme: { radius } }) => radius.small}; + color: ${({ theme: { color } }) => color.neutralText}; + background-color: ${({ theme: { color } }) => color.neutralBackground}; +`; + const drawLine = (lineNum: number) => { switch (lineNum) { case 1: diff --git a/fe/src/components/GameBoard/Roulette.tsx b/fe/src/components/GameBoard/Roulette.tsx index 6840070..2588526 100644 --- a/fe/src/components/GameBoard/Roulette.tsx +++ b/fe/src/components/GameBoard/Roulette.tsx @@ -1,8 +1,9 @@ import EventModal from '@components/Modal/EventModal/EventModal'; import useGetSocketUrl from '@hooks/useGetSocketUrl'; +import { usePlayerIdValue } from '@store/index'; import { useGameInfo, useResetEventRound } from '@store/reducer'; import { delay } from '@utils/index'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Wheel } from 'react-custom-roulette'; import { useParams } from 'react-router-dom'; import useWebSocket from 'react-use-websocket'; @@ -10,17 +11,55 @@ import { styled } from 'styled-components'; export default function Roulette() { const [mustSpin, setMustSpin] = useState(false); - const [isEventModalOpen, setIsEventModalOpen] = useState(false); const [prizeNumber, setPrizeNumber] = useState(0); + const [stockSellTime, setStockSellTime] = useState(30); + const [isEventModalOpen, setIsEventModalOpen] = useState(false); + const { gameId } = useParams(); const [gameInfo] = useGameInfo(); + const playerId = usePlayerIdValue(); const socketUrl = useGetSocketUrl(); const resetGameInfo = useResetEventRound(); - const { sendJsonMessage } = useWebSocket(socketUrl, { share: true, }); + const startSpin = useCallback(() => { + const eventListData = gameInfo.eventList.map((event) => event.title); + if (eventListData.length === 0) return; + if (gameInfo.firstPlayerId !== playerId) return; + const message = { + type: 'eventResult', + gameId, + events: eventListData, + }; + sendJsonMessage(message); + }, [ + gameId, + playerId, + gameInfo.eventList, + gameInfo.firstPlayerId, + sendJsonMessage, + ]); + + useEffect(() => { + let isMounted = true; + const stockSellTimer = async () => { + await delay(1000); + if (!isMounted) return; + if (stockSellTime > 0) { + setStockSellTime((prev) => prev - 1); + } else { + startSpin(); + } + }; + stockSellTimer(); + + return () => { + isMounted = false; + }; + }, [stockSellTime, startSpin]); + useEffect(() => { if (gameInfo.eventResult === '') return; const prizeNumber = gameInfo.eventList.findIndex( @@ -35,16 +74,6 @@ export default function Roulette() { return { option: event.title }; }); - const handleSpinClick = () => { - const eventListData = gameInfo.eventList.map((event) => event.title); - const message = { - type: 'eventResult', - gameId, - events: eventListData, - }; - sendJsonMessage(message); - }; - const handleSpinDone = async () => { setIsEventModalOpen(true); await delay(5000); @@ -61,20 +90,34 @@ export default function Roulette() { data={wheelData} fontSize={16} spinDuration={0.5} - textColors={['#fff', '#000']} + radiusLineWidth={2} + outerBorderWidth={2} pointerProps={{ style: { width: '70px', height: '70px' } }} + textColors={['#FCF5ED', '#000']} backgroundColors={['#3e3e3e', '#f4acb7']} onStopSpinning={handleSpinDone} /> - + + 남은 매도시간: {stockSellTime} + + {isEventModalOpen && } ); } +const Wrapper = styled.div` + width: 100%; + display: flex; + justify-content: space-around; +`; + +const Timer = styled.div` + font-size: ${({ theme }) => theme.fontSize.sMedium}; +`; + const Button = styled.button` width: 150px; - height: 100px; margin-right: 10px; margin-bottom: 10px; align-self: flex-end; diff --git a/fe/src/components/GameBoard/constants.ts b/fe/src/components/GameBoard/constants.ts index d2a2ea8..6d01dfb 100644 --- a/fe/src/components/GameBoard/constants.ts +++ b/fe/src/components/GameBoard/constants.ts @@ -32,7 +32,7 @@ export const initialBoard = [ { theme: 'pharmaceutical', logo: 'samsungbio', - name: '삼성바이오로직스', + name: '삼성바이오', location: 13, }, { theme: 'it', logo: 'google', name: '구글', location: 14 }, diff --git a/fe/src/components/Header/GameHeader.tsx b/fe/src/components/Header/GameHeader.tsx index 901464c..1ae2bc7 100644 --- a/fe/src/components/Header/GameHeader.tsx +++ b/fe/src/components/Header/GameHeader.tsx @@ -1,6 +1,5 @@ import StatusBoardModal from '@components/Modal/StatusBoardModal/StatusBoardModal'; import StockBuyModal from '@components/Modal/StockBuyModal/StockBuyModal'; -import StockSellModal from '@components/Modal/StockSellModal/StockSellModal'; import { Icon } from '@components/icon/Icon'; import useSound from '@hooks/useSound'; import { ROUTE_PATH } from '@router/constants'; @@ -12,7 +11,7 @@ export default function GameHeader() { const navigate = useNavigate(); const [isStatusBoardModalOpen, setIsStatusBoardModalOpen] = useState(false); const [isStockBuyModalOpen, setIsStockBuyModalOpen] = useState(false); - const [isStockSellModalOpen, setIsStockSellModalOpen] = useState(false); + const { isSoundPlaying, togglePlayingSound, @@ -33,16 +32,11 @@ export default function GameHeader() { setIsStockBuyModalOpen((prev) => !prev); }; - const toggleStockSellModal = () => { - setIsStockSellModalOpen((prev) => !prev); - }; - return ( <>
Gaemi Marble - 매도하기 칸도착 )} - {isStockSellModalOpen && ( - - )} {GameBgm} ); @@ -90,9 +81,9 @@ const Header = styled.div` } width: 100%; display: flex; - position: fixed; top: 0.5rem; padding: 0 2rem; + margin: 1rem 0; justify-content: space-between; `; diff --git a/fe/src/components/Layout.tsx b/fe/src/components/Layout.tsx index d815654..1ecf7c0 100644 --- a/fe/src/components/Layout.tsx +++ b/fe/src/components/Layout.tsx @@ -10,8 +10,10 @@ export default function Layout() { } const Page = styled.div` - width: 100vw; - height: 100vh; + width: max-content; + height: max-content; + min-width: 100vw; + min-height: 100vh; display: flex; flex-direction: column; color: ${({ theme: { color } }) => color.accentText}; diff --git a/fe/src/components/Modal/StockSellModal/StockSellModalContent.tsx b/fe/src/components/Modal/StockSellModal/StockSellModalContent.tsx index 4eeaa71..80aeed1 100644 --- a/fe/src/components/Modal/StockSellModal/StockSellModalContent.tsx +++ b/fe/src/components/Modal/StockSellModal/StockSellModalContent.tsx @@ -15,7 +15,7 @@ type StockSellModalContentProps = { export default function StockSellModalContent({ handleClose, }: StockSellModalContentProps) { - const { game, players, stocks } = useGameValue(); + const { players, stocks } = useGameValue(); const playerId = usePlayerIdValue(); const { gameId } = useParams(); const socketUrl = useGetSocketUrl(); @@ -86,27 +86,30 @@ export default function StockSellModalContent({ - {!!playerStocks?.length && + {playerStocks?.length ? ( playerStocks.map((stock) => ( - ))} + )) + ) : ( + + 보유한 주식이 없습니다. + + )} 총 매도 가격: {addCommasToNumber(totalPrice)} - {playerId === game.currentPlayerId && ( - - - - - )} + + + + ); } @@ -143,6 +146,12 @@ const StockInfoTable = styled.table` } `; +const EmptyTable = styled.tr` + td { + text-align: center; + } +`; + const ButtonWrapper = styled.div` display: flex; align-items: center; diff --git a/fe/src/components/Player/LeftPlayers.tsx b/fe/src/components/Player/LeftPlayers.tsx index 8d119df..89c2385 100644 --- a/fe/src/components/Player/LeftPlayers.tsx +++ b/fe/src/components/Player/LeftPlayers.tsx @@ -19,7 +19,7 @@ export default function LeftPlayers() { } const Players = styled.div` - width: 22rem; + min-width: 22rem; display: flex; flex-direction: column; align-items: center; diff --git a/fe/src/components/Player/PlayerCard.tsx b/fe/src/components/Player/PlayerCard.tsx index 3dcd75b..75bfd30 100644 --- a/fe/src/components/Player/PlayerCard.tsx +++ b/fe/src/components/Player/PlayerCard.tsx @@ -1,9 +1,11 @@ +import StockSellModal from '@components/Modal/StockSellModal/StockSellModal'; import { Icon } from '@components/icon/Icon'; import useClickScrollButton from '@hooks/useClickScrollButton'; import useGetSocketUrl from '@hooks/useGetSocketUrl'; import { usePlayerIdValue } from '@store/index'; import { useGameInfoValue } from '@store/reducer'; import { PlayerType } from '@store/reducer/type'; +import { useState } from 'react'; import { useParams } from 'react-router'; import useWebSocket from 'react-use-websocket'; import { styled } from 'styled-components'; @@ -17,6 +19,7 @@ type PlayerCardProps = { }; export default function PlayerCard({ player }: PlayerCardProps) { + const [isStockSellModalOpen, setIsStockSellModalOpen] = useState(false); const { ref, handleClickScroll } = useClickScrollButton({ width: SCROLL_ONCE, }); @@ -32,6 +35,8 @@ export default function PlayerCard({ player }: PlayerCardProps) { const isReady = player.isReady; const isMyButton = player.playerId === playerId; + const eventTime = gameInfo.currentPlayerId === null; + const beforeRouletteSpin = gameInfo.eventResult === ''; const handleReady = () => { const message = { @@ -43,6 +48,10 @@ export default function PlayerCard({ player }: PlayerCardProps) { sendJsonMessage(message); }; + const toggleStockSellModal = () => { + setIsStockSellModalOpen((prev) => !prev); + }; + return ( <> {player.playerId ? ( @@ -74,9 +83,19 @@ export default function PlayerCard({ player }: PlayerCardProps) { {isReady ? '준비완료' : '준비'} )} + {isMyButton && eventTime && beforeRouletteSpin && ( + + 매도하기 + + )} + {isStockSellModalOpen && ( + + )} ) : ( - + + + )} ); @@ -128,3 +147,14 @@ const Button = styled.button<{ $isReady: boolean }>` cursor: not-allowed; } `; + +const EmptyCardWrapper = styled(CardWrapper)` + margin: 4.5rem 0; +`; + +const StockSellButton = styled.button` + width: 6rem; + height: 3rem; + border: 1px solid; + border-radius: ${({ theme: { radius } }) => radius.small}; +`; diff --git a/fe/src/components/Player/RightPlayers.tsx b/fe/src/components/Player/RightPlayers.tsx index e742398..6c6a912 100644 --- a/fe/src/components/Player/RightPlayers.tsx +++ b/fe/src/components/Player/RightPlayers.tsx @@ -18,7 +18,7 @@ export default function RightPlayers() { } const Players = styled.div` - width: 22rem; + min-width: 22rem; display: flex; flex-direction: column; align-items: center; diff --git a/fe/src/pages/GamePage.tsx b/fe/src/pages/GamePage.tsx index a9d2f13..7a61e88 100644 --- a/fe/src/pages/GamePage.tsx +++ b/fe/src/pages/GamePage.tsx @@ -3,26 +3,16 @@ import GameHeader from '@components/Header/GameHeader'; import LeftPlayers from '@components/Player/LeftPlayers'; import RightPlayers from '@components/Player/RightPlayers'; import useGetSocketUrl from '@hooks/useGetSocketUrl'; -import { useGameInfoValue, usePlayersValue } from '@store/reducer'; import useGameReducer from '@store/reducer/useGameReducer'; import { useEffect } from 'react'; -import { useParams } from 'react-router-dom'; import useWebSocket from 'react-use-websocket'; import { styled } from 'styled-components'; export default function GamePage() { - const { gameId } = useParams(); - const playersInfo = usePlayersValue(); - const gameInfo = useGameInfoValue(); const { dispatch } = useGameReducer(); const socketUrl = useGetSocketUrl(); - // playersInfo에 빈 플레이어 객체일때는 isReady를 체크하지 않음 - const isEveryoneReady = playersInfo.every( - (player) => player.playerId === '' || player.isReady - ); - - const { sendJsonMessage, lastMessage } = useWebSocket(socketUrl, { + const { lastMessage } = useWebSocket(socketUrl, { share: true, }); @@ -37,14 +27,6 @@ export default function GamePage() { } }, [lastMessage]); - const handleStart = () => { - const message = { - type: 'start', - gameId, - }; - sendJsonMessage(message); - }; - return ( <> @@ -53,9 +35,6 @@ export default function GamePage() { - {!gameInfo.isPlaying && isEveryoneReady && ( - - )} @@ -67,28 +46,16 @@ const Container = styled.div` height: 100%; display: flex; flex-direction: column; - justify-content: center; align-items: center; - gap: 16px; + flex: 1; color: ${({ theme: { color } }) => color.accentText}; - background-color: ${({ theme: { color } }) => color.accentPrimary}; `; const Main = styled.div` width: 100%; + height: 100%; display: flex; justify-content: space-between; + flex: 1; padding: 0 1rem; `; - -const Button = styled.button` - width: 6rem; - height: 4rem; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - border-radius: ${({ theme: { radius } }) => radius.small}; - color: ${({ theme: { color } }) => color.neutralText}; - background-color: ${({ theme: { color } }) => color.neutralBackground}; -`; diff --git a/fe/src/pages/SignInPage.tsx b/fe/src/pages/SignInPage.tsx index 13bf446..db3cd68 100644 --- a/fe/src/pages/SignInPage.tsx +++ b/fe/src/pages/SignInPage.tsx @@ -86,6 +86,7 @@ const Container = styled.div` justify-content: center; align-items: center; gap: 16px; + flex: 1; `; const Title = styled.h1` diff --git a/fe/src/pages/SignUpPage.tsx b/fe/src/pages/SignUpPage.tsx index 217e8cd..12c39f6 100644 --- a/fe/src/pages/SignUpPage.tsx +++ b/fe/src/pages/SignUpPage.tsx @@ -70,6 +70,7 @@ const Container = styled.div` justify-content: center; align-items: center; gap: 16px; + flex: 1; `; const Title = styled.h1` diff --git a/fe/src/store/reducer/constants.ts b/fe/src/store/reducer/constants.ts index 251c86c..3a08b9b 100644 --- a/fe/src/store/reducer/constants.ts +++ b/fe/src/store/reducer/constants.ts @@ -222,7 +222,6 @@ export const initialGame = { dice: [0, 0], eventList: [], eventResult: '', - isSpin: false, isMoveFinished: false, teleportLocation: null, }; diff --git a/fe/src/store/reducer/index.ts b/fe/src/store/reducer/index.ts index 14fa11f..1fc4c50 100644 --- a/fe/src/store/reducer/index.ts +++ b/fe/src/store/reducer/index.ts @@ -29,6 +29,7 @@ const resetEventRoundAtom = atom(null, (_get, set) => { return { ...prev, dice: [0, 0], + eventList: [], eventResult: '', currentPlayerId: prev.firstPlayerId, }; diff --git a/fe/src/store/reducer/type.ts b/fe/src/store/reducer/type.ts index ead210e..8e24cb1 100644 --- a/fe/src/store/reducer/type.ts +++ b/fe/src/store/reducer/type.ts @@ -32,7 +32,6 @@ export type GameInfoType = { dice: number[]; eventList: RouletteEvent[]; eventResult: string; - isSpin: boolean; isMoveFinished: boolean; teleportLocation: number | null; };