diff --git a/src/api/axios.ts b/src/api/axios.ts index 344ab59..9a074c4 100644 --- a/src/api/axios.ts +++ b/src/api/axios.ts @@ -11,34 +11,52 @@ export const authInstance = axios.create({ authInstance.interceptors.request.use( (config) => { - const token = localStorage.getItem("accessToken") || ""; + const token = localStorage.getItem("accessToken"); if (token) { - config.headers["authorization"] = `${token}`; + config.headers.Authorization = token; // "Bearer ..." 형태로 저장돼 있다면 그대로 사용 } return config; }, (error) => { - console.error(error); return Promise.reject(error); }, ); -// authInstance.interceptors.response.use( -// (response) => response, -// async (error) => { -// const originalRequest = error.config; -// if (error.response.status === 401 && !originalRequest._retry) { -// originalRequest._retry = true; -// try { -// const newAccessToken = await refreshToken(); -// originalRequest.headers["Authorization"] = `${newAccessToken}`; -// return authInstance(originalRequest); -// } catch (refreshError) { -// localStorage.removeItem("accessToken"); -// alert("로그인을 다시 해주세요"); -// throw Promise.reject(refreshError); -// } -// } -// throw error; -// }, -// ); +authInstance.interceptors.response.use( + (response) => { + // 성공 응답은 그대로 반환 + return response; + }, + async (error) => { + const originalRequest = error.config; + + // 401 && 아직 재시도 안했을 경우 + if ( + error.response && + error.response.status === 401 && + !originalRequest._retry + ) { + originalRequest._retry = true; + try { + // Refresh Token으로 새 Access Token 발급 + const refreshResponse = await authInstance.post("/auth/refresh"); + const { accessToken } = refreshResponse.data; + + // 로컬스토리지에 다시 저장 + localStorage.setItem("accessToken", accessToken); + + // 원래 요청 헤더에 갱신된 토큰 반영 + originalRequest.headers.Authorization = accessToken; + + // 원래 요청 재시도 + return authInstance(originalRequest); + } catch (refreshError) { + // 재발급 실패: 로그인 페이지로 이동 등 처리 + // localStorage.clear() 또는 특정 에러 처리 + return Promise.reject(refreshError); + } + } + + return Promise.reject(error); + }, +); diff --git a/src/components/layout/gameRoomHeader/GameRoomHeader.tsx b/src/components/layout/gameRoomHeader/GameRoomHeader.tsx index 00d79c3..f69d897 100644 --- a/src/components/layout/gameRoomHeader/GameRoomHeader.tsx +++ b/src/components/layout/gameRoomHeader/GameRoomHeader.tsx @@ -1,17 +1,22 @@ import iconExit from "@assets/images/icon_close.svg"; -import { NavLink, useParams } from "react-router-dom"; +import { NavLink, useNavigate, useParams } from "react-router-dom"; import { Flex, StyledHeader, TextButton } from "./GameRoomHeaderStyle"; -import { useOutRoomMutaion } from "../../../hooks/useMutation"; + +import { useSocketStore } from "../../../store/useSocketStore"; export default function GameRoomHeader() { const params = useParams(); const roomId = Number(params.id); - const mutation = useOutRoomMutaion(); - - const onClickOut = (roomId: number) => { - mutation.mutate(roomId); - console.log("방나가기 성공asdfasdf"); + const navigate = useNavigate(); + const { socket } = useSocketStore(); + const onClickOut = async (roomId: number) => { + if (!socket) { + return; + } + socket.emit("leaveRoom", { roomId }); + navigate("/game"); }; + return ( <> diff --git a/src/components/ui/badge/Badge.tsx b/src/components/ui/badge/Badge.tsx index 4fd1922..3239416 100644 --- a/src/components/ui/badge/Badge.tsx +++ b/src/components/ui/badge/Badge.tsx @@ -1,9 +1,7 @@ import { IBadgeProps } from "./badge_props"; import * as S from "./badgeStyle"; -export default function Badge({ playerNumber }: IBadgeProps) { - const status = playerNumber === 2 ? "playing" : "waiting..."; - +export default function Badge({ playerNumber, status }: IBadgeProps) { return ( {status} diff --git a/src/components/ui/badge/badgeStyle.ts b/src/components/ui/badge/badgeStyle.ts index 37b4cbf..caf333e 100644 --- a/src/components/ui/badge/badgeStyle.ts +++ b/src/components/ui/badge/badgeStyle.ts @@ -28,6 +28,10 @@ export const Badge = styled.div` return ` background: radial-gradient(120% 120% at 50% 100%, #FF5E5E 0%, #FFFFFF 100%); `; + case "ready": + return ` + background: radial-gradient(120% 120% at 50% 100%, #2571FF 0%, #FFFFFF 100%); + `; } }}; `; diff --git a/src/components/ui/badge/badge_props.d.ts b/src/components/ui/badge/badge_props.d.ts index 25cc571..6923fbc 100644 --- a/src/components/ui/badge/badge_props.d.ts +++ b/src/components/ui/badge/badge_props.d.ts @@ -1,7 +1,7 @@ import { ReactNode } from "react"; export interface IBadgeProps { - status?: "waiting..." | "playing" | "not Ready"; + status?: "waiting..." | "playing" | "not Ready" | "ready"; children?: ReactNode; playerNumber?: number; } diff --git a/src/components/ui/card/profileCard/OtherProfileCard.tsx b/src/components/ui/card/profileCard/OtherProfileCard.tsx new file mode 100644 index 0000000..ea40d24 --- /dev/null +++ b/src/components/ui/card/profileCard/OtherProfileCard.tsx @@ -0,0 +1,34 @@ +import { ReactNode } from "react"; +import * as S from "./ProfileCardStyle"; +import { IUser } from "../../../../pages/game/GameRoom"; +import WaitingCard from "../waitingCard/WaitingCard"; +import OtherPlayerCard from "./playerCard/OtherPlayerCard"; + +interface IOtherProfileCard { + nickname?: string; + children: ReactNode; + roomId: number; + otherInfo?: IUser[]; + otherReady: boolean; +} + +export default function OtherProfileCard({ + nickname, + children, + otherInfo = [], + otherReady, +}: IOtherProfileCard) { + console.log(otherReady, "아더레디 확인"); + return ( +
+ {children} + + {otherInfo?.length > 0 ? ( + + ) : ( + + )} + +
+ ); +} diff --git a/src/components/ui/card/profileCard/ProfileCard.tsx b/src/components/ui/card/profileCard/ProfileCard.tsx index 28cffdb..44c46c8 100644 --- a/src/components/ui/card/profileCard/ProfileCard.tsx +++ b/src/components/ui/card/profileCard/ProfileCard.tsx @@ -2,17 +2,31 @@ import { ReactNode } from "react"; import PlayerCard from "./playerCard/playerCard"; import * as S from "./ProfileCardStyle"; -interface IProfileCard { - nickname: string | undefined; +interface IMyProfileCard { + nickname?: string; children: ReactNode; + roomId: number; + myReady: boolean; + handleMyReady: () => void; } -export default function ProfileCard({ nickname, children }: IProfileCard) { +export default function MyProfileCard({ + nickname, + children, + roomId, + myReady, + handleMyReady, +}: IMyProfileCard) { return (
{children} - +
); diff --git a/src/components/ui/card/profileCard/playerCard/OtherPlayerCard.tsx b/src/components/ui/card/profileCard/playerCard/OtherPlayerCard.tsx new file mode 100644 index 0000000..0f3f8b7 --- /dev/null +++ b/src/components/ui/card/profileCard/playerCard/OtherPlayerCard.tsx @@ -0,0 +1,18 @@ +import { Fragment } from "react/jsx-runtime"; + +import Profile from "../../../profile/Profile"; +import Badge from "../../../badge/Badge"; + +interface IPlayerCard { + nickname?: string; + otherReady: boolean; +} + +export default function OtherPlayerCard({ nickname, otherReady }: IPlayerCard) { + return ( + + + + + ); +} diff --git a/src/components/ui/card/profileCard/playerCard/playerCard.tsx b/src/components/ui/card/profileCard/playerCard/playerCard.tsx index 0be5335..aeefbdc 100644 --- a/src/components/ui/card/profileCard/playerCard/playerCard.tsx +++ b/src/components/ui/card/profileCard/playerCard/playerCard.tsx @@ -1,17 +1,54 @@ import { Fragment } from "react/jsx-runtime"; import Button from "../../../button/Button"; import Profile from "../../../profile/Profile"; +import { useSocketStore } from "../../../../../store/useSocketStore"; +import { useEffect } from "react"; + +interface IPlayerCard { + nickname?: string; + roomId: number; + handleMyReady: () => void; + myReady: boolean; +} export default function PlayerCard({ nickname, -}: { - nickname: string | undefined; -}) { + roomId, + myReady, + handleMyReady, +}: IPlayerCard) { + const { socket } = useSocketStore(); + + const onClickReady = () => { + if (!socket) { + return; + } + handleMyReady(); + }; + + useEffect(() => { + if (!socket) { + return; + } + if (myReady) { + console.log(myReady, "마이래디"); + socket?.emit("setReady", { roomId }); + } else { + socket?.emit("unReady", { roomId }); + } + }, [myReady]); + return ( - ); diff --git a/src/components/ui/card/waitingCard/WaitingCard.tsx b/src/components/ui/card/waitingCard/WaitingCard.tsx new file mode 100644 index 0000000..b6de94a --- /dev/null +++ b/src/components/ui/card/waitingCard/WaitingCard.tsx @@ -0,0 +1,11 @@ +import Badge from "../../badge/Badge"; +import * as S from "./waitingCardStyle"; + +export default function WaitingCard() { + return ( + + + 상대를 기다리는 중입니다. + + ); +} diff --git a/src/components/ui/card/waitingCard/waitingCardStyle.ts b/src/components/ui/card/waitingCard/waitingCardStyle.ts new file mode 100644 index 0000000..396fec1 --- /dev/null +++ b/src/components/ui/card/waitingCard/waitingCardStyle.ts @@ -0,0 +1,17 @@ +import styled from "styled-components"; + +export const container = styled.div` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 20px; +`; + +export const text = styled.div` + font-weight: 600; + font-size: 14px; + color: var(--color-gray-600); +`; diff --git a/src/pages/game/GameRoom.tsx b/src/pages/game/GameRoom.tsx index f18db9c..10e35c6 100644 --- a/src/pages/game/GameRoom.tsx +++ b/src/pages/game/GameRoom.tsx @@ -1,19 +1,21 @@ import { io } from "socket.io-client"; import ChattingBox from "../../components/ui/card/chatting/Chatting"; import { useSocketStore } from "../../store/useSocketStore"; -import ProfileCard from "../../components/ui/card/profileCard/ProfileCard"; import * as S from "./gameRoomStyle"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { jwtDecode } from "jwt-decode"; import { useGetGameRoomInfo } from "../../hooks/useQuery"; import { useParams } from "react-router-dom"; +import MyProfileCard from "../../components/ui/card/profileCard/ProfileCard"; +import OtherProfileCard from "../../components/ui/card/profileCard/OtherProfileCard"; -interface IUser { +export interface IUser { id: number; roomId: number; userId: number; joinedAt: string; userNickname: string; + ready: boolean; } interface IJoinRoomResponse { @@ -29,6 +31,28 @@ interface IJwtDecode { userId: number; } +interface IReadyData { + userId: number; + ready: boolean; +} + +interface IJoinData { + sender: string; + userNickname: string; + message: string; +} + +interface ILeaveData { + sender: string; + userNickname: string; + message: string; +} + +interface IGameStartData { + starterUserId: number; + message: string; +} + export default function GameRoom() { const { socket, setSocket } = useSocketStore(); const params = useParams(); @@ -36,10 +60,14 @@ export default function GameRoom() { const [userMyId, setUserMyId] = useState(undefined); const [myInfo, setMyInfo] = useState(undefined); const [otherInfo, setOtherInfo] = useState(undefined); - const accessToken = localStorage.getItem("accessToken"); + // const { setReady } = useReadyStore(); + const [myReady, setMyReady] = useState(false); + const [otherReady, setOtherReady] = useState(false); const { data, refetch } = useGetGameRoomInfo(roomId); + const userMyIdRef = useRef(undefined); useEffect(() => { + const accessToken = localStorage.getItem("accessToken"); if (!accessToken) { console.error("Access token is missing."); //오류 표시하거나 로그인창으로 리다이렉션 하기 @@ -48,6 +76,7 @@ export default function GameRoom() { const decoded = jwtDecode(accessToken); setUserMyId(decoded.userId); + userMyIdRef.current = decoded.userId; if (socket) { socket.on("connect", () => { @@ -59,6 +88,7 @@ export default function GameRoom() { extraHeaders: { Authorization: accessToken }, auth: { token: accessToken }, }); + setSocket(newSocket); newSocket.emit("joinRoom", { roomId }, (response: IJoinRoomResponse) => { @@ -82,19 +112,55 @@ export default function GameRoom() { useEffect(() => { if (!socket) return; - socket.on("join", async (data) => { + + const handleJoin = async (data: IJoinData) => { console.log(`${JSON.stringify(data)}`); const refetchInfo = await refetch(); - console.log(refetchInfo, "리페치정보"); - }); - }, [socket]); + console.log(refetchInfo, "Refetch Info"); + }; + + const handleLeave = async (data: ILeaveData) => { + console.log(`${JSON.stringify(data)}`); + }; + + const handleReady = async (data: IReadyData) => { + console.log(`${JSON.stringify(data)}`); + const { userId, ready } = data; + + if (userId !== userMyIdRef.current) { + setOtherReady(ready); + } + }; + + const handleGameStart = async (data: IGameStartData) => { + console.log(`${JSON.stringify(data)}`); + }; + + socket.on("join", handleJoin); + socket.on("leave", handleLeave); + socket.on("ready", handleReady); + socket.on("gameStart", handleGameStart); + + return () => { + socket.disconnect(); + }; + }, [socket, refetch]); + + const handleMyReady = () => { + setMyReady((prev) => !prev); + }; return ( - + 상대 플레이어 - + @@ -104,7 +170,14 @@ export default function GameRoom() { - + + 나 + ); diff --git a/src/pages/game/gameRoomStyle.ts b/src/pages/game/gameRoomStyle.ts index c647da9..7cc6079 100644 --- a/src/pages/game/gameRoomStyle.ts +++ b/src/pages/game/gameRoomStyle.ts @@ -10,7 +10,7 @@ export const container = styled.div` export const leftContainer = styled.div` width: 390px; - height: 1000px; + height: 100%; display: flex; flex-direction: column; justify-content: space-between; @@ -18,7 +18,7 @@ export const leftContainer = styled.div` `; export const centerContainer = styled.div` - height: 1000px; + height: 100%; width: 100%; background-color: green; position: relative; @@ -28,7 +28,7 @@ export const rightContainer = styled.div` margin-right: 77px; padding-bottom: 44px; width: 390px; - height: 1000px; + height: 100%; display: flex; gap: 40px; flex-direction: column; diff --git a/src/store/useReadyStore.ts b/src/store/useReadyStore.ts new file mode 100644 index 0000000..7a8fa24 --- /dev/null +++ b/src/store/useReadyStore.ts @@ -0,0 +1,11 @@ +import { create } from "zustand"; + +interface IReadyState { + ready: boolean; + setReady: (ready: boolean) => void; +} + +export const useReadyStore = create((set) => ({ + ready: false, + setReady: (ready: boolean) => set({ ready }), +}));