diff --git a/src/components/Game/Vote/index.tsx b/src/components/Game/Vote/index.tsx index e15c8cc8..4dcb2c44 100644 --- a/src/components/Game/Vote/index.tsx +++ b/src/components/Game/Vote/index.tsx @@ -55,6 +55,7 @@ const Vote: React.FC = ({ }; fetchDataFromFirebase(); + // eslint-disable-next-line }, []); const user = useRecoilValue(userState); diff --git a/src/components/Login/SignUpModal/index.tsx b/src/components/Login/SignUpModal/index.tsx index aa0f083b..3b1e87cc 100644 --- a/src/components/Login/SignUpModal/index.tsx +++ b/src/components/Login/SignUpModal/index.tsx @@ -1,25 +1,25 @@ -import React, { useCallback, useRef, useState } from "react"; -import { useForm, Controller } from "react-hook-form"; import { - Input, - Button, - Text, Alert, AlertIcon, - Modal, - ModalOverlay, - ModalContent, - ModalCloseButton, - ModalBody, + Box, + Button, FormControl, FormErrorMessage, - Box, Image, + Input, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalOverlay, + Spinner, + Text, } from "@chakra-ui/react"; -import useFetch from "../../../hooks/useFetch"; import axios from "axios"; -import styled from "styled-components"; +import React, { useCallback, useRef, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; import { FaImage } from "react-icons/fa"; +import styled from "styled-components"; interface FormData { id: string; @@ -56,19 +56,14 @@ const SignUpModal = ({ isOpen, onClose }: SignUpModalProps) => { clearErrors, formState: { errors }, } = useForm(); - const signUpFetch = useFetch({ - url: "https://fastcampus-chat.net/signup", - method: "POST", - data: {}, - start: false, - }); const [selectedFile, setSelectedFile] = useState(null); const [signUpStatus, setSignUpStatus] = useState<{ type: "success" | "error"; message: string; } | null>(null); const fileInputRef = useRef(null); - + const [isLoading, setIsLoading] = useState(false); + const MAX_IMAGE_SIZE = 1024 * 1024; // 1MB // ID 중복 검사 const checkIdDuplication = async (id: string): Promise => { try { @@ -86,6 +81,11 @@ const SignUpModal = ({ isOpen, onClose }: SignUpModalProps) => { const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files ? event.target.files[0] : null; if (file) { + if (file.size > MAX_IMAGE_SIZE) { + // 파일 크기 초과 에러 처리 + alert("이미지는 1MB 이하이어야 합니다."); + return; + } setSelectedFile(file); } }; @@ -110,6 +110,7 @@ const SignUpModal = ({ isOpen, onClose }: SignUpModalProps) => { // 회원가입 제출 const onSubmit = async (formData: FormData) => { + setIsLoading(true); // 로딩 시작 let pictureAsString: string | undefined; // 파일을 Base64 문자열로 변환 @@ -150,9 +151,20 @@ const SignUpModal = ({ isOpen, onClose }: SignUpModalProps) => { }); } } catch (error) { - console.error("회원가입 실패:", error); - setSignUpStatus({ type: "error", message: "회원가입에 실패하였습니다." }); + if (axios.isAxiosError(error) && error.response?.status === 401) { + setError("id", { + type: "manual", + message: "이미 사용중인 ID입니다.", + }); + } else { + console.error("회원가입 실패:", error); + setSignUpStatus({ + type: "error", + message: "회원가입에 실패하였습니다.", + }); + } } + setIsLoading(false); // 로딩 종료 }; return ( @@ -176,122 +188,125 @@ const SignUpModal = ({ isOpen, onClose }: SignUpModalProps) => { {signUpStatus.message} )} - {/* 회원가입 폼 */} -
- - {selectedFile ? ( - Upload Preview - ) : ( - - )} - 이미지 업로드 - - - {/* 아이디 입력 */} - - { - try { - const isDuplicated = await checkIdDuplication(id); - if (isDuplicated) { - setError("id", { - type: "manual", - message: "이미 사용중인 ID입니다.", - }); - return false; - } - clearErrors("id"); - return true; - } catch (error) { - console.error("ID 중복 확인 중 오류 발생:", error); - return "ID 중복 확인 중 오류가 발생했습니다."; - } - }, - }} - render={({ field }) => ( - + ) : ( + // 로딩이 완료되면 회원가입 내용 표시 + + + {selectedFile ? ( + Upload Preview + ) : ( + )} - /> - - {errors.id && errors.id.message} - - - {/* 비밀번호 입력 */} - - ( - - )} - /> - - {errors.password && errors.password.message} - - - {/* 닉네임 입력 */} - - ( - - )} - /> - - {errors.name && errors.name.message} - - - -
+ 이미지 업로드 + + + {/* 아이디 입력 */} + + { + try { + const isDuplicated = await checkIdDuplication(id); + if (isDuplicated) { + setError("id", { + type: "manual", + message: "이미 사용중인 ID입니다.", + }); + return false; + } + clearErrors("id"); + return true; + } catch (error) { + console.error("ID 중복 확인 중 오류 발생:", error); + return "ID 중복 확인 중 오류가 발생했습니다."; + } + }, + }} + render={({ field }) => ( + + )} + /> + + {errors.id && errors.id.message} + + + {/* 비밀번호 입력 */} + + ( + + )} + /> + + {errors.password && errors.password.message} + + + {/* 닉네임 입력 */} + + ( + + )} + /> + + {errors.name && errors.name.message} + + + + + )} diff --git a/src/components/Main/UserConfigModal/index.tsx b/src/components/Main/UserConfigModal/index.tsx index 4aacd9e2..6faa25e4 100644 --- a/src/components/Main/UserConfigModal/index.tsx +++ b/src/components/Main/UserConfigModal/index.tsx @@ -1,30 +1,28 @@ -import React, { useCallback, useRef, useState, useEffect } from "react"; -import { useForm, Controller } from "react-hook-form"; import { - Input, - Button, Alert, AlertIcon, - Modal, - ModalOverlay, - ModalContent, - ModalCloseButton, - ModalBody, + Box, + Button, FormControl, FormErrorMessage, - Box, Image, - Text, + Input, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalOverlay, Spinner, + Text, } from "@chakra-ui/react"; -import { useRecoilValue } from "recoil"; -import { authState } from "../../../recoil/atoms/authState"; import axios from "axios"; -import styled from "styled-components"; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; import { FaImage } from "react-icons/fa"; +import { useRecoilValue } from "recoil"; +import styled from "styled-components"; import useFetch from "../../../hooks/useFetch"; - -const MAX_IMAGE_SIZE = 1024 * 1024; // 1MB +import { authState } from "../../../recoil/atoms/authState"; interface FormData { id: string; @@ -32,6 +30,7 @@ interface FormData { picture?: string; } +const MAX_IMAGE_SIZE = 1024 * 1024; // 1MB const DragDropBox = styled(Box)` border: 3px dashed #dbdbdb; position: relative; diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 5bf3592d..56218207 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -5,6 +5,18 @@ import { userState } from "../recoil/atoms/userState"; export const useAuth = () => { const [auth, setAuth] = useRecoilState(authState); //사용자 인증 상태 const [user, setUser] = useRecoilState(userState); //사용자 정보 + + // 토큰 만료 여부 확인 + const isTokenExpired = (token: string) => { + try { + const payload = JSON.parse(atob(token.split(".")[1])); + return payload.exp < Date.now() / 1000; + } catch (error) { + return true; // 토큰 파싱에 실패하면 만료된 것으로 간주 + } + }; + + // 토큰 설정 및 저장 const setToken = ( accessToken: string, refreshToken: string, @@ -34,6 +46,7 @@ export const useAuth = () => { refreshToken: null, isAuthenticated: false, }); + setUser({ id: null, isLoggedIn: false }); // 사용자 상태 초기화 }; // 새로운 액세스 토큰 받아옴 @@ -60,9 +73,24 @@ export const useAuth = () => { } } catch (error) { console.error("Token refresh error:", error); - // 에러 핸들링 추가해야함. + logout(); } }; - return { auth, user, setToken, logout, refreshAccessToken }; + // API 요청 전 토큰 갱신(만료시 새 토큰 요청) + const refreshTokenIfNeeded = async () => { + const accessToken = localStorage.getItem("accessToken"); + if (accessToken && isTokenExpired(accessToken)) { + await refreshAccessToken(); + } + }; + + return { + auth, + user, + setToken, + logout, + refreshAccessToken, + refreshTokenIfNeeded, + }; }; diff --git a/src/recoil/atoms/userState.ts b/src/recoil/atoms/userState.ts index 87a4edde..85449701 100644 --- a/src/recoil/atoms/userState.ts +++ b/src/recoil/atoms/userState.ts @@ -1,7 +1,7 @@ import { atom } from "recoil"; export interface UserState { - id: string; + id: string | null; isLoggedIn: boolean; }