diff --git a/src/hooks/auth/useAuth.ts b/src/hooks/auth/useAuth.ts index dc0e39a..fbc576b 100644 --- a/src/hooks/auth/useAuth.ts +++ b/src/hooks/auth/useAuth.ts @@ -3,15 +3,15 @@ import { useCoreMutation } from "@/hooks/customQuery"; import { login, sendEmail, signUp, verifyEmail } from "@/api/auth/auth"; export const useAuth = () => { + const useLogin = useCoreMutation(login); + const useSignUp = useCoreMutation(signUp); const useSendCode = useCoreMutation(sendEmail); const useCheckCode = useCoreMutation(verifyEmail); - const useSignUp = useCoreMutation(signUp); - const useLogin = useCoreMutation(login); return { + useLogin, + useSignUp, useSendCode, useCheckCode, - useSignUp, - useLogin, }; }; diff --git a/src/hooks/auth/useEmailVerification.ts b/src/hooks/auth/useEmailVerification.ts index 721bdb8..ac39224 100644 --- a/src/hooks/auth/useEmailVerification.ts +++ b/src/hooks/auth/useEmailVerification.ts @@ -61,10 +61,10 @@ export const useEmailVerification = ({ useSendCode.mutate( { email }, { - onSuccess: () => { + onSuccess: (data) => { setSendCode(true); toast.success(successMessage); - restart(); + restart(data.data.expireIn); }, onError: (error) => { toast.error( diff --git a/src/hooks/auth/useSocialLogin.ts b/src/hooks/auth/useSocialLogin.ts new file mode 100644 index 0000000..f1c5838 --- /dev/null +++ b/src/hooks/auth/useSocialLogin.ts @@ -0,0 +1,13 @@ +import { type TSocialLoginPlatform } from "@/types/auth/auth"; + +export const useSocialLogin = () => { + const handleSocialLogin = (platform: TSocialLoginPlatform) => { + const API_BASE_URL = + import.meta.env.VITE_API_BASE_URL || import.meta.env.VITE_API_TARGET_URL; + + const baseUrl = `${API_BASE_URL}/oauth2/authorization/${platform}`; + window.location.href = `${baseUrl}`; + }; + + return { handleSocialLogin }; +}; diff --git a/src/hooks/common/useTimer.ts b/src/hooks/common/useTimer.ts index e1e676e..282694e 100644 --- a/src/hooks/common/useTimer.ts +++ b/src/hooks/common/useTimer.ts @@ -69,10 +69,13 @@ export const useTimer = ( setTimeLeft(Math.max(0, initialTime)); }, [initialTime]); - const restart = useCallback(() => { - setTimeLeft(Math.max(0, initialTime)); - setIsActive(true); - }, [initialTime]); + const restart = useCallback( + (newTime?: number) => { + setTimeLeft(Math.max(0, newTime ?? initialTime)); + setIsActive(true); + }, + [initialTime], + ); const formattedTime = useMemo(() => { const minutes = Math.floor(timeLeft / 60); diff --git a/src/lib/axiosInstance.ts b/src/lib/axiosInstance.ts index 0342a6d..cde2d72 100644 --- a/src/lib/axiosInstance.ts +++ b/src/lib/axiosInstance.ts @@ -67,7 +67,7 @@ axiosInstance.interceptors.response.use( originalRequest.url?.includes("/api/auth/reissue") || originalRequest._retry ) { - useAuthStore.getState().logout(); + localStorage.removeItem("accessToken"); return Promise.reject(error); } diff --git a/src/pages/auth/Login.tsx b/src/pages/auth/Login.tsx index dccc00f..57e3c40 100644 --- a/src/pages/auth/Login.tsx +++ b/src/pages/auth/Login.tsx @@ -7,6 +7,7 @@ import type { z } from "zod"; import { loginSchema } from "@/utils/validation"; import { useAuth } from "@/hooks/auth/useAuth"; +import { useSocialLogin } from "@/hooks/auth/useSocialLogin"; import CommonAuthInput from "@/components/auth/common/CommonAuthInput"; import Button from "@/components/common/Button"; @@ -31,13 +32,13 @@ export default function Login() { const navigate = useNavigate(); const { useLogin } = useAuth(); const { login: loginAction } = useAuthStore(); + const { handleSocialLogin } = useSocialLogin(); const onSubmit: SubmitHandler = (data) => { useLogin.mutate(data, { onSuccess: (response) => { const { accessToken } = response.data; - localStorage.setItem("accessToken", accessToken); - loginAction(data.email); + loginAction(data.email, accessToken); navigate("/", { replace: true }); }, onError: (error: any) => { @@ -92,9 +93,7 @@ export default function Login() { type="button" className="w-14 h-14 rounded-full flex items-center justify-center bg-social-kakao hover:scale-110 transition-transform duration-200 shadow-sm" aria-label="카카오로 로그인" - onClick={() => { - // 소셜 로그인 - }} + onClick={() => handleSocialLogin("kakao")} > @@ -103,9 +102,7 @@ export default function Login() { type="button" className="w-14 h-14 rounded-full flex items-center justify-center bg-social-naver hover:scale-110 transition-transform duration-200 shadow-sm" aria-label="네이버로 로그인" - onClick={() => { - // 소셜 로그인 - }} + onClick={() => handleSocialLogin("naver")} > @@ -114,9 +111,7 @@ export default function Login() { type="button" className="w-14 h-14 rounded-full flex items-center justify-center bg-white border border-gray-100 hover:scale-110 transition-transform duration-200 shadow-sm" aria-label="구글로 로그인" - onClick={() => { - // 소셜 로그인 - }} + onClick={() => toast.error("준비중입니다.")} > diff --git a/src/pages/auth/RedirectPage.tsx b/src/pages/auth/RedirectPage.tsx new file mode 100644 index 0000000..be011a8 --- /dev/null +++ b/src/pages/auth/RedirectPage.tsx @@ -0,0 +1,46 @@ +import { useEffect, useRef } from "react"; +import { useNavigate } from "react-router-dom"; +import { toast } from "sonner"; + +import useAuthStore from "@/store/useAuthStore"; + +export default function RedirectPage() { + const navigate = useNavigate(); + const { login, setSocialId, setEmail } = useAuthStore(); + const processed = useRef(false); + + useEffect(() => { + if (processed.current) return; + processed.current = true; + + const getCookie = (name: string) => { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop()?.split(";").shift(); + }; + + const accessToken = getCookie("access_token"); + + if (accessToken) { + // TODO: Zustand 로그인 상태 업데이트 - 추후 내 정보 조회 API 연동 시 수정 + login("social@user.com", accessToken); + + toast.success("소셜 로그인되었습니다."); + navigate("/", { replace: true }); + } else { + console.error("No access token found in cookies."); + toast.error("소셜 로그인에 실패했습니다. 다시 시도해주세요."); + navigate("/login", { replace: true }); + } + }, [navigate, login, setEmail, setSocialId]); + + return ( +
+
+
+ 로그인 중... +
+
+
+ ); +} diff --git a/src/pages/auth/Signup.tsx b/src/pages/auth/Signup.tsx index 5fb092a..23fa626 100644 --- a/src/pages/auth/Signup.tsx +++ b/src/pages/auth/Signup.tsx @@ -1,6 +1,8 @@ import { useEffect, useState } from "react"; import { Link, useLocation } from "react-router-dom"; +import { useSocialLogin } from "@/hooks/auth/useSocialLogin"; + import Step01Email from "@/components/auth/signupStep/Step01Email"; import Step02Password from "@/components/auth/signupStep/Step02Password"; import Step03Profile from "@/components/auth/signupStep/Step03Profile"; @@ -31,6 +33,8 @@ export default function Signup() { setStep((prev) => prev + 1); }; + const { handleSocialLogin } = useSocialLogin(); + if (step === 1) { return ; } @@ -60,7 +64,7 @@ export default function Signup() { size="big" variant="custom" leftIcon={} - onClick={() => {}} + onClick={() => handleSocialLogin("google")} className="bg-white border border-gray-100 text-text-main font-heading3 shadow-sm hover:bg-gray-50" > 구글 로그인 @@ -71,7 +75,7 @@ export default function Signup() { size="big" variant="custom" leftIcon={} - onClick={() => {}} + onClick={() => handleSocialLogin("kakao")} className="bg-social-kakao text-text-main font-heading3 shadow-sm hover:opacity-90" > 카카오 로그인 @@ -82,7 +86,7 @@ export default function Signup() { size="big" variant="custom" leftIcon={} - onClick={() => {}} + onClick={() => handleSocialLogin("naver")} className="bg-social-naver text-white font-heading3 shadow-sm hover:opacity-90" > 네이버 로그인 diff --git a/src/routes/AuthRoutes.tsx b/src/routes/AuthRoutes.tsx index 0191d25..40e6ee3 100644 --- a/src/routes/AuthRoutes.tsx +++ b/src/routes/AuthRoutes.tsx @@ -21,6 +21,11 @@ const Login = loadable( , ); +const RedirectPage = loadable( + lazy(() => import("@/pages/auth/RedirectPage")), + , +); + // Signup은 Fallback이 달라짐 -> raw lazy 컴포넌트 사용 const Signup = lazy(() => import("@/pages/auth/Signup")); @@ -54,6 +59,10 @@ const AuthRoutes: RouteObject[] = [ path: "find-pw", element: , }, + { + path: "oauth2/redirect", + element: , + }, ]; export default AuthRoutes; diff --git a/src/store/useAuthStore.ts b/src/store/useAuthStore.ts index 7795ea9..bb19dff 100644 --- a/src/store/useAuthStore.ts +++ b/src/store/useAuthStore.ts @@ -5,7 +5,8 @@ interface IAuthState { email: string; password: string; socialId: number; - login: (email: string) => void; + + login: (email: string, accessToken: string) => void; logout: () => void; setEmail: (email: string) => void; setPassword: (password: string) => void; @@ -18,11 +19,15 @@ const useAuthStore = create((set) => ({ email: "", password: "", socialId: -1, - login: (email) => set({ isLoggedIn: true, email }), + login: (email, accessToken) => { + localStorage.setItem("accessToken", accessToken); + set({ isLoggedIn: true, email }); + }, logout: () => { localStorage.removeItem("accessToken"); set({ isLoggedIn: false, email: "", password: "", socialId: -1 }); }, + setEmail: (email) => set({ email }), setPassword: (password) => set({ password }), setSocialId: (socialId) => set({ socialId }), diff --git a/src/types/auth/auth.ts b/src/types/auth/auth.ts index 4909acd..32be6fc 100644 --- a/src/types/auth/auth.ts +++ b/src/types/auth/auth.ts @@ -1,22 +1,22 @@ -// 이메일 인증 코드 전송 요청 타입 +// 이메일 인증 전송 요청 export interface IEmailSendRequest { email: string; } -// 이메일 인증 코드 전송 응답 타입 +// 이메일 인증 전송 응답 export interface IEmailSendResponse { message: string; email: string; expireIn: number; } -// 이메일 인증 코드 확인 요청 타입 +// 이메일 인증 확인 요청 export interface IEmailVerifyRequest { email: string; authCode: string; } -// 회원가입 요청 타입 +// 회원가입 요청 export interface ISignUpRequest { email: string; password: string; @@ -24,33 +24,37 @@ export interface ISignUpRequest { phone_number: string; } -// 회원가입 응답 타입 +// 회원가입 응답 export interface ISignUpResponse { userId: number; createdAt: string; } -// 로그인 요청 타입 +// 로그인 요청 export interface ILoginRequest { email: string; password: string; } +export type TLoginFormValues = ILoginRequest; -// 로그인 응답 타입 +// 로그인 응답 export interface ILoginResponse { grantType: string; accessToken: string; accessTokenExpiresIn: number; } -// 토큰 재발급 요청 타입 +// 토큰 재발급 요청 export interface ITokenRefreshRequest { refreshToken: string; } -// 토큰 재발급 응답 타입 +// 토큰 재발급 응답 export interface ITokenRefreshResponse { grantType: string; accessToken: string; accessTokenExpiresIn: number; } + +// 소셜 로그인 플랫폼 +export type TSocialLoginPlatform = "kakao" | "naver" | "google";