From 9e3074f20536fbb79549700439da261db184e4b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=EC=A7=80=EC=9C=A4?= <70828192+cheonjiyun@users.noreply.github.com> Date: Sun, 22 Dec 2024 22:13:29 +0900 Subject: [PATCH] Mafia-together ver 2.0 prod deploy (#154) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat : 서버 api 요청 주소 변경 * chore : dev cicd 파이프라인 작성 * chore : dev CI/CD 파이프라인 수정 * chore : prod CI/CD 작성 * refactor: 채팅을 웹소켓으로 교체한다 (#138) * fix: 새로고침하면 본인 직업이 날라가는 문제 해결 (#140) * fix: 새로고침 할때 가끔 채팅구독이 안되는 문제 해결 (#142) 연결될때 useState를 변경해서 구독을 트리거 함 * feat: 대기방 정보 SSE로 교체 (#144) * feat: 대기방 정보 SSE로 교체 * chore: 사용안하는 코드 및 log제거 * feat: 마피아 스킬을 웹소켓으로 교체 (#146) * Feature/148 채팅에서 직업 보여주기 (#150) * chore: 글씨 제거 * feat: 채팅에서 직업 찾아서 보여주기 * Feature/#147 깃허브 액션에 env 추가 (#151) * feat: 초대코드 도메인 두번 들어가는 형상 제거 * feat: 웹소켓 baseURL에 도메인 변수를 활용하도록 수정 * feat: 도메인 상수를 env를 활용하도록 수정 * feat: 깃허브 액션에 env 추가 * fix: 마피아일 때 기존 타켓 요청이 안가는 버그 수정 --------- Co-authored-by: waterricecake Co-authored-by: waterricecake <91263263+waterricecake@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/frontend-dev-CICD.yml | 36 +++++ .github/workflows/frontend-prod-CICD.yml | 42 +++++ .gitignore | 3 +- package-lock.json | 6 + package.json | 1 + src/axios/http.ts | 27 +--- src/axios/instances.ts | 3 +- src/components/chat/Chat.tsx | 45 ++++++ src/components/chat/ChatForm.tsx | 10 +- src/components/chat/ChatGroup.tsx | 15 +- src/components/chat/Chats.tsx | 11 +- src/components/job/MafiaNight.tsx | 64 +++++--- src/components/modal/NoticeMyJob.tsx | 16 +- src/pages/Day.tsx | 28 +--- src/pages/Game.tsx | 193 +++++++++++++++++++++-- src/pages/Night.tsx | 11 +- src/pages/Result.tsx | 6 +- src/pages/WaitingRoom.tsx | 30 ++-- src/recoil/roominfo/atom.ts | 4 +- src/type/index.ts | 33 +++- src/util/job.ts | 5 + 22 files changed, 458 insertions(+), 133 deletions(-) create mode 100644 .github/workflows/frontend-dev-CICD.yml create mode 100644 .github/workflows/frontend-prod-CICD.yml create mode 100644 src/components/chat/Chat.tsx create mode 100644 src/util/job.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2cca22c..df21d1f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: '14' + node-version: '20' - name: Install dependencies run: npm install diff --git a/.github/workflows/frontend-dev-CICD.yml b/.github/workflows/frontend-dev-CICD.yml new file mode 100644 index 0000000..9e2c59e --- /dev/null +++ b/.github/workflows/frontend-dev-CICD.yml @@ -0,0 +1,36 @@ +name: Dev CI/CD + +on: + push: + branches: [dev] + +permissions: + contents: read + +jobs: + build: + runs-on: + group: Default + labels: [self-hosted, dev] + permissions: + pull-requests: write + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Install dependencies + run: npm install + + - name: Run build + run: npm run build + env: + VITE_DOMAIN: ${{ secrets.VITE_DOMAIN_DEV }} + + - name: 빌드 파일 + run: cp -rf ./dist ~/ diff --git a/.github/workflows/frontend-prod-CICD.yml b/.github/workflows/frontend-prod-CICD.yml new file mode 100644 index 0000000..b9f658f --- /dev/null +++ b/.github/workflows/frontend-prod-CICD.yml @@ -0,0 +1,42 @@ +name: Prod CI/CD + +on: + push: + branches: [prod] + +permissions: + contents: read + +jobs: + build: + runs-on: + group: Default + labels: [self-hosted, dev] + permissions: + pull-requests: write + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Install dependencies + run: npm install + + - name: Run build + run: npm run build + env: + VITE_DOMAIN: ${{ secrets.VITE_DOMAIN_PROD }} + + - name: Copy to Prod Server + uses: appleboy/scp-action@master + with: + username: ${{ secrets.PROD_USER_NAME }} + host: ${{ secrets.PROD_SERVER }} + key: ${{ secrets.PROD_PEM_KEY }} + source: './dist' + target: '~/' diff --git a/.gitignore b/.gitignore index 8834d89..d90a4c2 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,5 @@ dist-ssr *.sw? # env -.env.development \ No newline at end of file +.env +.env.development diff --git a/package-lock.json b/package-lock.json index 67f7978..1721dba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", + "@stomp/stompjs": "^7.0.0", "@tanstack/react-query": "^5.32.0", "axios": "^1.6.8", "event-source-polyfill": "^1.0.31", @@ -1195,6 +1196,11 @@ "win32" ] }, + "node_modules/@stomp/stompjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.0.0.tgz", + "integrity": "sha512-fGdq4wPDnSV/KyOsjq4P+zLc8MFWC3lMmP5FBgLWKPJTYcuCbAIrnRGjB7q2jHZdYCOD5vxLuFoKIYLy5/u8Pw==" + }, "node_modules/@swc/core": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.15.tgz", diff --git a/package.json b/package.json index 46061dc..b306a6c 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dependencies": { "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", + "@stomp/stompjs": "^7.0.0", "@tanstack/react-query": "^5.32.0", "axios": "^1.6.8", "event-source-polyfill": "^1.0.31", diff --git a/src/axios/http.ts b/src/axios/http.ts index ec4c3b9..698f64a 100644 --- a/src/axios/http.ts +++ b/src/axios/http.ts @@ -1,7 +1,7 @@ import { useSuspenseQuery } from '@tanstack/react-query'; import { - Chat, + ChatArray, ChatRequest, DeadResult, GameExist, @@ -41,33 +41,10 @@ export const getValidRoomCode = async (code: string | null) => { return http.get(`/lobbies/code/exist?code=${code}`); }; -export const useChatsQuery = () => { - const { data: chats, ...rest } = useSuspenseQuery({ - queryKey: ['chats', localStorage.getItem('auth')], - queryFn: () => getChats(), - refetchInterval: 500, - staleTime: 500, - }); - return { - chats, - ...rest, - }; -}; - export const getRoomsStatus = () => { return http.get('/games/status'); }; -export const useGamesInfoQuery = () => { - const { data: gameInfo, ...rest } = useSuspenseQuery({ - queryKey: ['games', 'info', localStorage.getItem('auth')], - queryFn: () => getGamesInfo(), - refetchInterval: 500, - staleTime: 500, - }); - return { gameInfo, ...rest }; -}; - export const getGamesInfo = () => { return http.get(`/games/info`); }; @@ -85,7 +62,7 @@ export const existGame = () => { }; export const getChats = () => { - return http.get(`/chat`); + return http.get(`/chat`); }; export const postChats = (payload: ChatRequest) => { diff --git a/src/axios/instances.ts b/src/axios/instances.ts index eece91b..d279280 100644 --- a/src/axios/instances.ts +++ b/src/axios/instances.ts @@ -1,6 +1,7 @@ import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; -export const BASE_URL = 'https://mafia-together.com/api'; +export const DOMAIN = import.meta.env.VITE_DOMAIN; +export const BASE_URL = `https://${DOMAIN}/api`; export const axiosInstance = axios.create({ baseURL: BASE_URL, diff --git a/src/components/chat/Chat.tsx b/src/components/chat/Chat.tsx new file mode 100644 index 0000000..a8b4a79 --- /dev/null +++ b/src/components/chat/Chat.tsx @@ -0,0 +1,45 @@ +/** @jsxImportSource @emotion/react */ +import { css } from '@emotion/react'; +import { useRecoilState } from 'recoil'; + +import { roomInfoState } from '../../recoil/roominfo/atom'; +import { VariablesCSS } from '../../styles/VariablesCSS'; +import { ChatArray } from '../../type'; +import { ChatForm } from './ChatForm'; +import { Chats } from './Chats'; + +type PropsType = { + publishChat: (content: string) => void; + chats: ChatArray; + setChats: React.Dispatch>; +}; + +export const Chat = ({ publishChat, chats, setChats }: PropsType) => { + /* 방 정보 */ + const [roomInfo] = useRecoilState(roomInfoState); + + // 내가 살아있는지 + const isAlive = roomInfo?.isAlive; + + return ( + <> + {/* 채팅목록 */} +
+ +
+ + {/* 살아있는 경우에만 input창이 보인다. */} + {isAlive && } + + ); +}; + +const middle = css` + height: calc(100% - ${VariablesCSS.top} - 55px - 20px); + overflow: scroll; + -ms-overflow-style: none; + scrollbar-width: none; + &::-webkit-scrollbar { + display: none; + } +`; diff --git a/src/components/chat/ChatForm.tsx b/src/components/chat/ChatForm.tsx index 9049ff7..90306e6 100644 --- a/src/components/chat/ChatForm.tsx +++ b/src/components/chat/ChatForm.tsx @@ -2,7 +2,6 @@ import { css } from '@emotion/react'; import { useState } from 'react'; -import { postChats } from '../../axios/http'; import { VariablesCSS } from '../../styles/VariablesCSS'; function isInvalidInputChat(inputChat: string) { @@ -10,14 +9,19 @@ function isInvalidInputChat(inputChat: string) { return inputChat.length === 0; } -export const ChatForm = () => { +type PropsType = { + publishChat: (content: string) => void; +}; + +export const ChatForm = ({ publishChat }: PropsType) => { const [inputChat, setInputChat] = useState(''); return (
{ event.preventDefault(); - postChats({ contents: inputChat }); + publishChat(inputChat); + // postChats({ content: inputChat }); setInputChat(''); }} > diff --git a/src/components/chat/ChatGroup.tsx b/src/components/chat/ChatGroup.tsx index 5b3f3e5..bdb21d7 100644 --- a/src/components/chat/ChatGroup.tsx +++ b/src/components/chat/ChatGroup.tsx @@ -1,9 +1,12 @@ /** @jsxImportSource @emotion/react */ import { css } from '@emotion/react'; import { forwardRef } from 'react'; +import { useRecoilState } from 'recoil'; +import { roomInfoState } from '../../recoil/roominfo/atom'; import { VariablesCSS } from '../../styles/VariablesCSS'; import { Chat } from '../../type'; +import { getPlayerJob } from '../../util/job'; import PlayerChat from '../player/PlayerChat'; import ChatMessage from './ChatMessage'; @@ -15,13 +18,19 @@ interface PropsType { export default forwardRef(function ChatGroup(props: PropsType, ref: any) { const { chats } = props; + const [roomInfo] = useRecoilState(roomInfoState); + return (
- +

{chats[0].name}

- {chats.map(chat => ( - + {chats.map((chat, idx) => ( + ))}
diff --git a/src/components/chat/Chats.tsx b/src/components/chat/Chats.tsx index f4253cc..bef3ab5 100644 --- a/src/components/chat/Chats.tsx +++ b/src/components/chat/Chats.tsx @@ -1,13 +1,16 @@ import { useEffect, useRef } from 'react'; -import { useChatsQuery } from '../../axios/http'; -import { Chat } from '../../type'; +import { Chat, ChatArray } from '../../type'; import ChatGroup from './ChatGroup'; -export const Chats = () => { +type PropsType = { + chats: ChatArray; + setChats: React.Dispatch>; +}; + +export const Chats = ({ chats }: PropsType) => { const chatRef = useRef(null); - const { chats } = useChatsQuery(); useEffect(() => { if (!chatRef.current) return; chatRef.current.scrollIntoView({ block: 'end' }); diff --git a/src/components/job/MafiaNight.tsx b/src/components/job/MafiaNight.tsx index 796293d..13f4129 100644 --- a/src/components/job/MafiaNight.tsx +++ b/src/components/job/MafiaNight.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/react'; import { useEffect, useState } from 'react'; -import { postSkill, useMafiaVoteResultQuery } from '../../axios/http'; +// import { postSkill, useMafiaVoteResultQuery } from '../../axios/http'; import { middle } from '../../pages/Night'; import { VariablesCSS } from '../../styles/VariablesCSS'; import { Player } from '../../type'; @@ -13,32 +13,48 @@ import PlayerNight from '../player/PlayerNight'; interface PropsType { isAlive: boolean; players: Player[]; + publishSkill: (name: string) => void; + mafiaSkillPlayer: string | null; } -export const MafiaNight = (props: PropsType) => { - const { players, isAlive } = props; - - const { mafiaVoteResult } = useMafiaVoteResultQuery(); - let nowVoteResult = mafiaVoteResult.target === '' ? 0 : -1; - players.forEach((player, i) => { - if (player.name === mafiaVoteResult.target) { - nowVoteResult = i + 1; - } - }); +export const MafiaNight = ({ isAlive, players, publishSkill, mafiaSkillPlayer }: PropsType) => { + // 지금 투표중인사람 const [check, setCheck] = useState(-1); + useEffect(() => { - (async () => { - if (check === -1) { + setCheck(mafiaSkillPlayer === '' ? 0 : -1); + + players.forEach((player, i) => { + if (player.name === mafiaSkillPlayer) { + setCheck(i + 1); + } + }); + }, [mafiaSkillPlayer, players]); + + // let nowVoteResult = mafiaSkillPlayer === '' ? 0 : -1; + + // 이름 -> index로 변경 + + const findTargetName = (): string => { + let targetName = ''; + players.forEach((player, i) => { + if (check === i + 1) { + targetName = player.name; return; } - let targetName = ''; - players.forEach((player, i) => { - if (check === i + 1) { - targetName = player.name; - return; - } - }); - await postSkill({ target: targetName }); - })(); + }); + return targetName; + }; + + const skill = async () => { + if (check === -1) { + return; + } + const targetName = findTargetName(); + publishSkill(targetName); + }; + + useEffect(() => { + skill(); }, [check, players]); return ( @@ -53,7 +69,7 @@ export const MafiaNight = (props: PropsType) => { key={i + 1} index={i + 1} myJob={'MAFIA'} - nowVoteResult={nowVoteResult} + nowVoteResult={check} {...(isAlive && { setCheck: setCheck })} /> ))} @@ -65,7 +81,7 @@ export const MafiaNight = (props: PropsType) => { name="vote" id="0" css={notkill} - checked={nowVoteResult === 0} + checked={check === 0} onChange={() => isAlive && setCheck(0)} /> diff --git a/src/components/modal/NoticeMyJob.tsx b/src/components/modal/NoticeMyJob.tsx index 9c0b2c9..8b34c37 100644 --- a/src/components/modal/NoticeMyJob.tsx +++ b/src/components/modal/NoticeMyJob.tsx @@ -1,12 +1,9 @@ /** @jsxImportSource @emotion/react */ import { css } from '@emotion/react'; -import { useEffect, useState } from 'react'; -import { useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; -import { getMyJob } from '../../axios/http'; import { myJobState } from '../../recoil/roominfo/atom'; import { VariablesCSS } from '../../styles/VariablesCSS'; -import { Job } from '../../type'; import PlayerBig from '../player/PlayerBig'; import NoticeCitizen from './NoticeJobs/NoticeCitizen'; import NoticeMafia from './NoticeJobs/NoticeMafia'; @@ -25,16 +22,7 @@ const text = { export default function NoticeMyJob(props: PropsType) { const { name } = props; - // 내 직업공지 - const [myJob, setMyJob] = useState('CITIZEN'); - const setMyJobRecoilState = useSetRecoilState(myJobState); // 방 정보 - useEffect(() => { - (async () => { - const myJobResponse = await getMyJob(); - setMyJob(myJobResponse.job); - setMyJobRecoilState(myJobResponse.job); - })(); - }, []); + const myJob = useRecoilValue(myJobState); return ( <> diff --git a/src/pages/Day.tsx b/src/pages/Day.tsx index 001db28..d357e48 100644 --- a/src/pages/Day.tsx +++ b/src/pages/Day.tsx @@ -3,8 +3,7 @@ import { css } from '@emotion/react'; import { Suspense, useEffect, useState } from 'react'; import { useRecoilState } from 'recoil'; -import { ChatForm } from '../components/chat/ChatForm'; -import { Chats } from '../components/chat/Chats'; +import { Chat } from '../components/chat/Chat'; import { Loading } from '../components/etc/Loading'; import AppContainerCSS from '../components/layout/AppContainerCSS'; import ModalContainer from '../components/modal/ModalContainer'; @@ -16,19 +15,21 @@ import VoteResult from '../components/modal/VoteResult'; import TopDay from '../components/top/TopDay'; import { gameRound, roomInfoState } from '../recoil/roominfo/atom'; import { VariablesCSS } from '../styles/VariablesCSS'; -import { Status } from '../type'; +import { ChatArray, Status } from '../type'; type PropsType = { statusType: Status; + publishChat: (content: string) => void; + chats: ChatArray; + setChats: React.Dispatch>; }; -export default function Day({ statusType }: PropsType) { +export default function Day({ statusType, publishChat, chats, setChats }: PropsType) { // 라운드 (몇일차) const [gameRoundState] = useRecoilState(gameRound); /* 방 정보 */ const [roomInfo] = useRecoilState(roomInfoState); - // 내가 살아있는지 const isAlive = roomInfo?.isAlive; @@ -64,12 +65,7 @@ export default function Day({ statusType }: PropsType) { statusType={statusType} /> -
- -
- - {/* 살아있는 경우에만 input창이 보인다. */} - {isAlive && } + {/* 공지 모달 TIME*/} @@ -131,13 +127,3 @@ const gameMessage = css` color: ${VariablesCSS.day}; animation: smoothshow 0.8s; `; - -const middle = css` - height: calc(100% - ${VariablesCSS.top} - 55px - 20px); - overflow: scroll; - -ms-overflow-style: none; - scrollbar-width: none; - &::-webkit-scrollbar { - display: none; - } -`; diff --git a/src/pages/Game.tsx b/src/pages/Game.tsx index e3ac92d..113e6b2 100644 --- a/src/pages/Game.tsx +++ b/src/pages/Game.tsx @@ -1,18 +1,43 @@ +import * as StompJs from '@stomp/stompjs'; import { EventListener, EventSourcePolyfill } from 'event-source-polyfill'; import { useEffect, useRef, useState } from 'react'; -import { useSetRecoilState } from 'recoil'; +import { useRecoilState, useSetRecoilState } from 'recoil'; -import { getGamesInfo } from '../axios/http'; -import { BASE_URL } from '../axios/instances'; -import { gameRound, roomInfoState } from '../recoil/roominfo/atom'; +import { getChats, getGamesInfo, getMyJob } from '../axios/http'; +import { BASE_URL, DOMAIN } from '../axios/instances'; +import { gameRound, myJobState, roomInfoState } from '../recoil/roominfo/atom'; +import { ChatArray, ChatResponse, GameStatus, SkillResponse, WaitingRoomInfo } from '../type'; import Day from './Day'; import Night from './Night'; import Result from './Result'; import WaitingRoom from './WaitingRoom'; export default function Game() { + const auth = localStorage.getItem('auth'); + const [chats, setChats] = useState([]); + const socketClientState = useRef(null); + + const [chatSubscribeId, setChatSubscribeId] = useState(null); + const [skillSubscribeId, setSkillSubscribeId] = useState(null); + const [mafiaSkillPlayer, setMafiaSkillPlayer] = useState(null); + const [roomsInfoState, setRoomsInfoState] = useRecoilState(roomInfoState); // 방 정보 + const [waitingRoomInfoState, setWaitingRoomInfoState] = useState({ + totalPlayers: 1, + isMaster: true, + myName: '내이름', + lobbyPlayerResponses: [ + { + name: '이름', + }, + ], + }); + const [finishSocketConneted, setFinishSocketConnetd] = useState(false); // 웹 소켓 연결이 끝난다는 트리거(채팅 구독이 연결 전에 실행될 때를 대비해 다시 실행하기 위함) + + const setGameRoundState = useSetRecoilState(gameRound); + // 방 상태 불러오기 - const [gamesStatus, setGameStatus] = useState({ statusType: 'WAIT' }); + const [gamesStatus, setGameStatus] = useState({ statusType: 'WAIT' }); + const [myJobRecoilState, setMyJobRecoilState] = useRecoilState(myJobState); // SSE const eventSource = useRef(null); @@ -32,20 +57,149 @@ export default function Game() { setGameStatus(JSON.parse(response.data)); }) as EventListener); + eventSource.current.addEventListener('lobbyInfo', ((response: MessageEvent) => { + setWaitingRoomInfoState(JSON.parse(response.data)); + }) as EventListener); + return () => { eventSource.current?.close(); }; }, []); - // 방 정보 저장 (방 상태가 바뀔때만 작동?) - const setRoomsInfoState = useSetRecoilState(roomInfoState); // 방 정보 + // WebSocket + const connect = () => { + const socket = new StompJs.Client({ + brokerURL: `wss://${DOMAIN}/api/stomp`, + reconnectDelay: 10000, + }); - const setGameRoundState = useSetRecoilState(gameRound); + if (!socket.active) { + socket.activate(); + } + + socket.onConnect = () => { + setFinishSocketConnetd(true); + }; + + socketClientState.current = socket; + }; + + const disConnect = () => { + socketClientState.current?.deactivate(); + }; + + // 웹소켓 연결 + useEffect(() => { + connect(); + return () => disConnect(); + }, []); + + // 채팅구독함수 + const subscribeChat = () => { + if (!socketClientState.current?.connected) return; + const chatSubscribeId = socketClientState.current.subscribe(`/sub/chat/${auth}`, response => { + const msg: ChatResponse = JSON.parse(response.body); + + const isOwner = msg.name == roomsInfoState.myName; + setChats(chats => [...chats, { ...msg, isOwner: isOwner }]); + }); + + setChatSubscribeId(chatSubscribeId); + }; + + // 채팅구독끊기 + const unsubscribeChat = () => { + if (!socketClientState.current?.connected) return; + chatSubscribeId?.unsubscribe(); + }; + + // 채팅보내기 + const publishChat = (content: string) => { + if (!socketClientState.current?.connected) return; + + socketClientState.current.publish({ + destination: `/pub/chat/${auth}`, + body: JSON.stringify({ content: content }), + }); + }; + + // 채팅구독하기 + useEffect(() => { + unsubscribeChat(); + if (gamesStatus.statusType !== 'DAY') return; + + // 본래 채팅불러오기 + (async () => { + const response = await getChats(); + setChats(response); + })(); + + subscribeChat(); + return () => unsubscribeChat(); + }, [gamesStatus.statusType, finishSocketConneted]); + + // ====== + // 밤 직업구독 함수 + const subscribeSkill = async () => { + if (!socketClientState.current?.connected) return; + + const mafiaSubscribeId = socketClientState.current.subscribe(`/sub/mafia/${auth}`, response => { + const msg: SkillResponse = JSON.parse(response.body); + setMafiaSkillPlayer(msg.content); + }); + + setSkillSubscribeId(mafiaSubscribeId); + }; + + // 밤 직업 구독끊기 + const unsubscribeSkill = () => { + if (!socketClientState.current?.connected) return; + skillSubscribeId?.unsubscribe(); + }; + + // 밤 스킬 + const publishSkill = (name: string) => { + if (!socketClientState.current?.connected) return; + + socketClientState.current.publish({ + destination: `/pub/skill/${auth}`, + body: JSON.stringify({ target: name }), + }); + }; + + useEffect(() => { + unsubscribeSkill(); + if (gamesStatus.statusType !== 'NIGHT') return; + + subscribeSkill(); + return () => unsubscribeSkill(); + }, [gamesStatus.statusType, finishSocketConneted]); + + // 방 정보 저장 (방 상태가 바뀔때만 작동?) useEffect(() => { - // 방 정보 불러오기 (async () => { + // 방 정보 불러오기 const roomInfoResponse = await getGamesInfo(); setRoomsInfoState(roomInfoResponse); + + // 대기방 SSE이벤트를 받기전에 처음값 + setWaitingRoomInfoState({ + ...waitingRoomInfoState, + totalPlayers: roomInfoResponse.totalPlayers, + isMaster: roomInfoResponse.isMaster, + myName: roomInfoResponse.myName, + lobbyPlayerResponses: roomInfoResponse.players.map(player => { + return { name: player.name }; + }), + }); + + // 내 직업 + if (gamesStatus.statusType !== 'WAIT' && !myJobRecoilState) { + const myJobResponse = await getMyJob(); + console.log(myJobResponse); + + setMyJobRecoilState(myJobResponse.job); + } })(); // DAY로 바뀔때 마다 라운드 +1 @@ -54,18 +208,31 @@ export default function Game() { } else if (gamesStatus.statusType === 'WAIT') { setGameRoundState(0); } - }, [gamesStatus.statusType, setGameRoundState, setRoomsInfoState]); + }, [gamesStatus.statusType, setGameRoundState, setMyJobRecoilState, setRoomsInfoState]); return ( <> - {gamesStatus.statusType === 'WAIT' && } + {gamesStatus.statusType === 'WAIT' && ( + + )} {(gamesStatus.statusType === 'DAY_INTRO' || gamesStatus.statusType === 'NOTICE' || gamesStatus.statusType === 'DAY' || gamesStatus.statusType === 'VOTE' || - gamesStatus.statusType === 'VOTE_RESULT') && } + gamesStatus.statusType === 'VOTE_RESULT') && ( + + )} {(gamesStatus.statusType === 'NIGHT_INTRO' || gamesStatus.statusType === 'NIGHT') && ( - + )} {gamesStatus.statusType === 'END' && } diff --git a/src/pages/Night.tsx b/src/pages/Night.tsx index 7cd6b74..e033b59 100644 --- a/src/pages/Night.tsx +++ b/src/pages/Night.tsx @@ -16,9 +16,11 @@ import { Status } from '../type'; type PropsType = { statusType: Status; + publishSkill: (name: string) => void; + mafiaSkillPlayer: string | null; }; -export default function Night({ statusType }: PropsType) { +export default function Night({ statusType, publishSkill, mafiaSkillPlayer }: PropsType) { const [roomInfo] = useRecoilState(roomInfoState); const [myJob] = useRecoilState(myJobState); @@ -39,7 +41,12 @@ export default function Night({ statusType }: PropsType) { <> {'MAFIA' === myJob && ( - + )} {'CITIZEN' === myJob && ( diff --git a/src/pages/Result.tsx b/src/pages/Result.tsx index d85c811..4965741 100644 --- a/src/pages/Result.tsx +++ b/src/pages/Result.tsx @@ -69,9 +69,9 @@ export default function Result() { )} diff --git a/src/pages/WaitingRoom.tsx b/src/pages/WaitingRoom.tsx index 4c47836..fa3c6fb 100644 --- a/src/pages/WaitingRoom.tsx +++ b/src/pages/WaitingRoom.tsx @@ -4,7 +4,7 @@ import { Suspense, useEffect, useState } from 'react'; import { Toaster } from 'react-hot-toast'; import { useNavigate } from 'react-router-dom'; -import { getRoomsCode, startGame, useGamesInfoQuery } from '../axios/http'; +import { getRoomsCode, startGame } from '../axios/http'; import BigButton from '../components/button/BigButton'; import { Loading } from '../components/etc/Loading'; import AppContainerCSS from '../components/layout/AppContainerCSS'; @@ -13,17 +13,21 @@ import PlayerWaiting from '../components/player/PlayerWaiting'; import { notifyUseToast } from '../components/toast/NotifyToast'; import TopEnter from '../components/top/TopEnter'; import { VariablesCSS } from '../styles/VariablesCSS'; -import { Player } from '../type'; +import { lobbyPlayer, WaitingRoomInfo } from '../type'; -export default function WaitingRoom() { +type PropsType = { + waitingRoomInfoState: WaitingRoomInfo; +}; + +export default function WaitingRoom({ waitingRoomInfoState }: PropsType) { const [openAnimation, setOpenAnimation] = useState(false); /* 참가목록 받아오기 */ - const [players, setPlayers] = useState([]); - const { gameInfo } = useGamesInfoQuery(); + const [players, setPlayers] = useState([]); const getVirtualPlayers = () => { - const virtualPlayersLength = gameInfo.totalPlayers - gameInfo.players.length; + const virtualPlayersLength = + waitingRoomInfoState.totalPlayers - waitingRoomInfoState.lobbyPlayerResponses.length; const virtualPlayer = { name: '', isAlive: true, @@ -33,8 +37,8 @@ export default function WaitingRoom() { }; useEffect(() => { - setPlayers(gameInfo.players); - }, [gameInfo.players]); + setPlayers(waitingRoomInfoState.lobbyPlayerResponses); + }, [waitingRoomInfoState]); /* 초대하기 모달 */ // 띄우고 끄기 @@ -65,8 +69,8 @@ export default function WaitingRoom() { }; const onShareLink = async () => { - // 링크 공유 - const inviteLink = 'https://mafia-together.com' + '/#/participate?code=' + code; + // 링크 공유s + const inviteLink = '/#/participate?code=' + code; const shareData = { url: inviteLink, }; @@ -81,7 +85,7 @@ export default function WaitingRoom() { /* 게임시작 */ const canStartGame = () => { - return gameInfo.isMaster && players.length === gameInfo.totalPlayers; + return waitingRoomInfoState.isMaster && players.length === waitingRoomInfoState.totalPlayers; }; const navigate = useNavigate(); const onGameStart = async () => { @@ -100,7 +104,7 @@ export default function WaitingRoom() {

참가목록

- {players.length}/{gameInfo.totalPlayers} + {players.length}/{waitingRoomInfoState.totalPlayers}

@@ -110,7 +114,7 @@ export default function WaitingRoom() {
- {gameInfo.isMaster && ( + {waitingRoomInfoState.isMaster && ( )}
diff --git a/src/recoil/roominfo/atom.ts b/src/recoil/roominfo/atom.ts index 08dea3f..f18a656 100644 --- a/src/recoil/roominfo/atom.ts +++ b/src/recoil/roominfo/atom.ts @@ -1,6 +1,6 @@ import { atom } from 'recoil'; -import { Job, GameInfo } from '../../type'; +import { GameInfo, Job } from '../../type'; export const gameRound = atom({ key: 'gameRound', @@ -29,5 +29,5 @@ export const roomInfoState = atom({ export const myJobState = atom({ key: 'myJobState', - default: 'CITIZEN', + default: null, }); diff --git a/src/type/index.ts b/src/type/index.ts index fdcddb6..72c6f15 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -18,16 +18,32 @@ export interface GameStatus { statusType: Status; } +export type ChatArray = Chat[]; + export interface Chat { name: string; - contents: string; - timestamp: Date; + content: string; + timeStamp: Date; isOwner: boolean; job: Job; } +export interface ChatResponse { + name: string; + content: string; + timeStamp: Date; + job: Job; +} + export interface ChatRequest { - contents: string; + content: string; +} + +export interface SkillResponse { + name: string; + content: string; + messageType: string; + timeStamp: Date; } export interface RoomResponse { @@ -64,6 +80,17 @@ export interface GameInfo { players: Player[]; } +export interface WaitingRoomInfo { + totalPlayers: number; + isMaster: boolean; + myName: string; + lobbyPlayerResponses: lobbyPlayer[]; +} + +export interface lobbyPlayer { + name: string; +} + export interface MyJobResponse { job: Job; } diff --git a/src/util/job.ts b/src/util/job.ts new file mode 100644 index 0000000..4d24d35 --- /dev/null +++ b/src/util/job.ts @@ -0,0 +1,5 @@ +import { Job, Player } from '../type'; + +export const getPlayerJob = (players: Player[], name: string): Job => { + return players.find(el => el.name == name)?.job || null; +};