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.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;
};