From e3091494b1c9456d852679674c353a7af8345cfc Mon Sep 17 00:00:00 2001 From: geonhee-min Date: Wed, 3 Dec 2025 12:59:08 +0900 Subject: [PATCH] =?UTF-8?q?issue=20#32=20-=20=EC=83=88=EB=A1=9C=EA=B3=A0?= =?UTF-8?q?=EC=B9=A8=20=EC=8B=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=95=B4?= =?UTF-8?q?=EC=A0=9C=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0=20=EB=B0=8F?= =?UTF-8?q?=20=EC=9E=90=EB=8F=99=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 32 +++++++++++--- src/network/BaseNetwork.ts | 58 +++++++++++++++++-------- src/store/authStore.ts | 35 +++++++++------ src/ui/page/account/login/LoginPage.tsx | 22 +++++++++- 4 files changed, 108 insertions(+), 39 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 8b595bf..3a0742f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,19 +7,39 @@ import { PageRouting } from './const/PageRouting'; import LoginPage from './ui/page/account/login/LoginPage'; import ResetPasswordPage from './ui/page/account/resetPassword/ResetPasswordPage'; import { HomePage } from './ui/page/home/HomePage'; +import type { AuthData } from './data/AuthData'; +import { useEffect } from 'react'; function App() { - const { authData } = useAuthStore(); + const { authData, login } = useAuthStore(); + useEffect(() => { + const autoLogin = localStorage.getItem('autoLogin') === 'true'; + if (autoLogin) { + const stored = localStorage.getItem('auth-storage'); + if (stored) { + const storedAuthData = JSON.parse(stored).state as AuthData; + login(storedAuthData); + } + } + }, []); return ( }> - } path={PageRouting["LOGIN"].path} /> - } path={PageRouting["SIGN_UP"].path} /> - } path={PageRouting["RESET_PASSWORD"].path} /> - {!authData ? } path="*" /> : null} - } path={PageRouting["HOME"].path} /> + { + !authData + ? <> + } path={PageRouting["LOGIN"].path} /> + } path={PageRouting["SIGN_UP"].path} /> + } path={PageRouting["RESET_PASSWORD"].path} /> + } path="*" /> + + : <> + } path="*" /> + } path={PageRouting["HOME"].path} /> + + } diff --git a/src/network/BaseNetwork.ts b/src/network/BaseNetwork.ts index 202eb0e..7dce6eb 100644 --- a/src/network/BaseNetwork.ts +++ b/src/network/BaseNetwork.ts @@ -7,7 +7,8 @@ import type { InternalAxiosRequestConfig, } from "axios"; import { useAuthStore } from '@/store/authStore'; -import type { RefreshAccessTokenResponse } from '@/data/response/account/RefreshAccessTokenResponse'; +import { RefreshAccessTokenResponse } from '@/data/response/account/RefreshAccessTokenResponse'; +import type { AuthData } from '@/data/AuthData'; export class BaseNetwork { protected instance: AxiosInstance; @@ -98,30 +99,16 @@ export class BaseNetwork { this.isRefreshing = true; try { - const response = await this.get( - '/account/refresh-access-token', - { - headers: { - Authorization: `Bearer ${refreshToken}` - } - } - ) + await this.refreshToken(); - const newAccessToken = response.data.accessToken; - const newRefreshToken = response.data.refreshToken; - - useAuthStore.getState().login({ - ...authData, - accessToken: newAccessToken, - refreshToken: newRefreshToken - }); + const newAccessToken = useAuthStore.getState().authData!.accessToken; this.refreshQueue.forEach((cb) => cb(newAccessToken)); this.refreshQueue = []; originalRequest.headers = { ...originalRequest.headers, Authorization: `Bearer ${newAccessToken}`, - }; + } as any; return this.instance(originalRequest); } catch (err) { @@ -143,4 +130,39 @@ export class BaseNetwork { protected async post(url: string, data?: any, config?: AxiosRequestConfig & { authPass?: boolean }) { return await this.instance.post(url, data, config); } + + public async refreshToken() { + const storedAuth = localStorage.getItem('auth-storage'); + + if (!storedAuth) { + localStorage.setItem('autoLogin', 'false'); + throw new Error; + } + + const authData: AuthData = JSON.parse(storedAuth).state; + + if (!authData || !authData.refreshToken) { + localStorage.setItem('autoLogin', 'false'); + throw new Error; + } + + const result = await this.get( + '/account/refresh-access-token', + { + headers: { + Authorization: `Bearer ${authData.refreshToken}` + } + } + ); + + if (!result.data.success) throw new Error; + + const newAccessToken = result.data.accessToken; + const newRefreshToken = result.data.refreshToken; + + useAuthStore.getState().login({ + accessToken: newAccessToken, + refreshToken: newRefreshToken + }); + } } \ No newline at end of file diff --git a/src/store/authStore.ts b/src/store/authStore.ts index 7ba1bf7..53bba8c 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -1,5 +1,6 @@ import type { AuthData } from '@/data/AuthData'; import { create } from 'zustand'; +import { createJSONStorage, persist } from 'zustand/middleware'; interface AuthStoreProps { authData: AuthData | undefined; @@ -7,17 +8,23 @@ interface AuthStoreProps { logout: () => void; } -export const useAuthStore = create((set) => ({ - authData: undefined, - login: (data: AuthData) => { - set({ authData: data }); - Object.entries(data) - .forEach((entry) => { - localStorage.setItem(entry[0], entry[1]); - }) - }, - logout: () => { - set({ authData: undefined }); - localStorage.clear(); - } -})); \ No newline at end of file +const storage = sessionStorage; +export const useAuthStore = create()( + persist( + (set) => ({ + authData: undefined, + login: (data: AuthData) => { + set({ authData: data }); + }, + logout: () => { + localStorage.setItem('autoLogin', 'false'); + localStorage.removeItem('auth-storage'); + set({ authData: undefined }); + } + }), + { + name: 'auth-storage', + storage: createJSONStorage(() => storage) + } + ) +); \ No newline at end of file diff --git a/src/ui/page/account/login/LoginPage.tsx b/src/ui/page/account/login/LoginPage.tsx index 8c8a861..eefe02d 100644 --- a/src/ui/page/account/login/LoginPage.tsx +++ b/src/ui/page/account/login/LoginPage.tsx @@ -4,7 +4,7 @@ import { Field, FieldError, FieldLabel } from '@/components/ui/field'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { zodResolver } from '@hookform/resolvers/zod'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import { PageRouting } from '@/const/PageRouting'; @@ -15,9 +15,12 @@ import { LoginRequest } from '@/data/request/account/LoginRequest'; import { AccountNetwork } from '@/network/AccountNetwork'; import { toast } from 'sonner'; import { useAuthStore } from '@/store/authStore'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Label } from '@/components/ui/label'; export default function LoginPage() { const [isLoading, setIsLoading] = useState(false); + const [autoLogin, setAutoLogin] = useState(false); const { login } = useAuthStore(); const navigate = useNavigate(); const accountNetwork = new AccountNetwork(); @@ -30,6 +33,10 @@ export default function LoginPage() { }); const { id, password } = { id: loginForm.watch('id'), password: loginForm.watch('password') }; + useEffect(() => { + localStorage.setItem('autoLogin', `${autoLogin}`) + }, [autoLogin]); + const moveToSignUpPage = useCallback(() => { navigate(PageRouting["SIGN_UP"].path); }, []); @@ -149,7 +156,20 @@ export default function LoginPage() { )} > +
+ setAutoLogin(value === true)} + /> + +
+