diff --git a/src/_apis/auth/logout-apis.ts b/src/_apis/auth/logout-apis.ts index e69de29b..cbc45f2d 100644 --- a/src/_apis/auth/logout-apis.ts +++ b/src/_apis/auth/logout-apis.ts @@ -0,0 +1,10 @@ +import { fetchApi } from '@/src/utils/api'; + +export function logout(): Promise { + return fetchApi('/auths/logout', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); +} diff --git a/src/_apis/auth/reissue-apis.ts b/src/_apis/auth/reissue-apis.ts new file mode 100644 index 00000000..48f393a0 --- /dev/null +++ b/src/_apis/auth/reissue-apis.ts @@ -0,0 +1,18 @@ +import { fetchApi } from '@/src/utils/api'; + +export async function reissue(): Promise<{ token: string | null }> { + return fetchApi<{ headers: Headers }>( + '/auths/reissue', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, + 5000, + true, + ).then((response) => { + const token = response.headers.get('Authorization'); + return { token }; + }); +} diff --git a/src/_queries/auth/login-queries.ts b/src/_queries/auth/login-queries.ts index 7b65170b..b19234fe 100644 --- a/src/_queries/auth/login-queries.ts +++ b/src/_queries/auth/login-queries.ts @@ -1,8 +1,8 @@ import { useMutation } from '@tanstack/react-query'; import { login } from '@/src/_apis/auth/login-apis'; +import { useHandleAuthSuccess } from '@/src/hooks/use-handle-auth-success'; import { ApiError } from '@/src/utils/api'; import { LoginRequest, LoginResponse } from '@/src/types/auth'; -import { useHandleAuthSuccess } from './use-handle-auth-success'; export function usePostLoginQuery() { const handleAuthSuccess = useHandleAuthSuccess(); diff --git a/src/_queries/auth/logout-queries.ts b/src/_queries/auth/logout-queries.ts index e69de29b..17a1a4bb 100644 --- a/src/_queries/auth/logout-queries.ts +++ b/src/_queries/auth/logout-queries.ts @@ -0,0 +1,9 @@ +import { useMutation } from '@tanstack/react-query'; +import { logout } from '@/src/_apis/auth/logout-apis'; +import { ApiError } from '@/src/utils/api'; + +export function usePostLogoutQuery() { + return useMutation({ + mutationFn: logout, + }); +} diff --git a/src/_queries/auth/reissue-queries.ts b/src/_queries/auth/reissue-queries.ts new file mode 100644 index 00000000..ac41cdf6 --- /dev/null +++ b/src/_queries/auth/reissue-queries.ts @@ -0,0 +1,15 @@ +import { useMutation } from '@tanstack/react-query'; +import { reissue } from '@/src/_apis/auth/reissue-apis'; +import { useHandleAuthSuccess } from '@/src/hooks/use-handle-auth-success'; +import { ApiError } from '@/src/utils/api'; + +export function usePostReissueQuery() { + const handleAuthSuccess = useHandleAuthSuccess(); + + return useMutation<{ token: string | null }, ApiError>({ + mutationFn: reissue, + onSuccess: async (response) => { + await handleAuthSuccess(response.token); + }, + }); +} diff --git a/src/_queries/auth/signup-queries.ts b/src/_queries/auth/signup-queries.ts index eb39a5a1..c6c20217 100644 --- a/src/_queries/auth/signup-queries.ts +++ b/src/_queries/auth/signup-queries.ts @@ -1,8 +1,8 @@ import { useMutation } from '@tanstack/react-query'; import { signup } from '@/src/_apis/auth/signup-apis'; +import { useHandleAuthSuccess } from '@/src/hooks/use-handle-auth-success'; import { ApiError } from '@/src/utils/api'; import { SignupRequest, SignupResponse } from '@/src/types/auth'; -import { useHandleAuthSuccess } from './use-handle-auth-success'; export function usePostSignupQuery() { const handleAuthSuccess = useHandleAuthSuccess(); diff --git a/src/_queries/auth/user-apis.ts b/src/_queries/auth/user-apis.ts deleted file mode 100644 index cc339273..00000000 --- a/src/_queries/auth/user-apis.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { getUser } from '@/src/_apis/auth/user-apis'; -import { transformKeysToCamel } from '@/src/utils/transform-keys'; -import { User } from '@/src/types/auth'; - -export function getUserQuery() { - return { - queryKey: ['user'], - queryFn: getUser, - select: (data: User) => transformKeysToCamel(data), - }; -} diff --git a/src/_queries/auth/user-queries.ts b/src/_queries/auth/user-queries.ts new file mode 100644 index 00000000..687c1b06 --- /dev/null +++ b/src/_queries/auth/user-queries.ts @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query'; +import { getUser } from '@/src/_apis/auth/user-apis'; +import { authStore } from '@/src/store/use-auth-store'; + +export function useUser() { + const { token } = authStore.getState(); + return useQuery({ + queryKey: ['user'], + queryFn: getUser, + enabled: !!token, + }); +} diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx index 3cef2e25..9865f6e0 100644 --- a/src/app/(auth)/layout.tsx +++ b/src/app/(auth)/layout.tsx @@ -1,3 +1,4 @@ +import { Suspense } from 'react'; import type { Metadata } from 'next'; import Image from 'next/image'; import '@mantine/core/styles.css'; @@ -30,7 +31,9 @@ export default function AuthLayout({
함께할 사람이없나요? 지금 크루에 참여해보세요
-
{children}
+
+ {children} +
diff --git a/src/app/(auth)/login/_component/login-form.tsx b/src/app/(auth)/login/_component/login-form.tsx index 8fe169b5..e634f835 100644 --- a/src/app/(auth)/login/_component/login-form.tsx +++ b/src/app/(auth)/login/_component/login-form.tsx @@ -1,6 +1,7 @@ -import { UseFormReturn } from 'react-hook-form'; -import { Button } from '@mantine/core'; -import { useDebouncedCallback } from '@mantine/hooks'; +import { useEffect } from 'react'; +import { UseFormReturn, useWatch } from 'react-hook-form'; +import { useDebouncedValue } from '@mantine/hooks'; +import Button from '@/src/components/common/input/button'; import PasswordInput from '@/src/components/common/input/password-input'; import TextInput from '@/src/components/common/input/text-input'; @@ -18,15 +19,29 @@ export default function LoginForm({ onSubmit, formMethods }: LoginFormProps) { register, handleSubmit, trigger, + control, formState: { errors, isValid }, } = formMethods; - const debouncedTrigger = useDebouncedCallback(async (field: keyof LoginFormValues) => { - await trigger(field); - }, 1000); + const email = useWatch({ control, name: 'email' }); + const password = useWatch({ control, name: 'password' }); + + const [debouncedEmail, cancelDebouncedEmail] = useDebouncedValue(email, 1000); + const [debouncedPassword, cancelDebouncedPassword] = useDebouncedValue(password, 1000); + + useEffect(() => { + if (debouncedEmail) trigger('email'); + if (debouncedPassword) trigger('password'); + }, [debouncedEmail, debouncedPassword, trigger]); + + const handleFormSubmit = handleSubmit(async (data) => { + onSubmit(data); + cancelDebouncedEmail(); + cancelDebouncedPassword(); + }); return ( -
+ trigger('email'), - onChange: async () => debouncedTrigger('email'), }), }} error={errors.email?.message} @@ -56,7 +70,6 @@ export default function LoginForm({ onSubmit, formMethods }: LoginFormProps) { message: '비밀번호가 8자 이상이 되도록 해주세요.', }, onBlur: () => trigger('password'), - onChange: async () => debouncedTrigger('password'), }), }} error={errors.password?.message} @@ -65,8 +78,11 @@ export default function LoginForm({ onSubmit, formMethods }: LoginFormProps) { />
- {/* TODO: Button 바꾸기 */} -
diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index f58b42a4..a01675c3 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -1,22 +1,29 @@ 'use client'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import Link from 'next/link'; -import { useRouter } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import { usePostLoginQuery } from '@/src/_queries/auth/login-queries'; import LoginForm, { LoginFormValues } from './_component/login-form'; export default function LoginPage() { + const [redirect, setRedirect] = useState('/'); + const searchParams = useSearchParams(); const router = useRouter(); const formMethods = useForm(); const { setError } = formMethods; const { mutate: postLogin } = usePostLoginQuery(); + useEffect(() => { + const redirectParam = searchParams.get('redirect'); + if (redirectParam) setRedirect(redirectParam); + }, [searchParams]); + const handleSubmit = async (data: LoginFormValues) => { postLogin(data, { onSuccess: () => { - router.push('/'); + router.push(redirect); }, onError: (error) => { if (error.status === 401) { @@ -38,7 +45,10 @@ export default function LoginPage() {
크루가 처음이신가요?
- + 회원가입
diff --git a/src/app/(auth)/signup/_component/signup-form.tsx b/src/app/(auth)/signup/_component/signup-form.tsx index aaee00c3..9879f074 100644 --- a/src/app/(auth)/signup/_component/signup-form.tsx +++ b/src/app/(auth)/signup/_component/signup-form.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; -import { UseFormReturn } from 'react-hook-form'; -import { Button } from '@mantine/core'; -import { useDebouncedCallback } from '@mantine/hooks'; +import { UseFormReturn, useWatch } from 'react-hook-form'; +import { useDebouncedValue } from '@mantine/hooks'; +import Button from '@/src/components/common/input/button'; import PasswordInput from '@/src/components/common/input/password-input'; import TextInput from '@/src/components/common/input/text-input'; @@ -22,24 +22,52 @@ export default function SignupForm({ formMethods, onSubmit }: SignupFormProps) { register, handleSubmit, trigger, + control, formState: { errors, isValid }, - watch, } = formMethods; - const debouncedTrigger = useDebouncedCallback(async (field: keyof SignupFormValues) => { - await trigger(field); - }, 1000); + const [nickname, email, password, confirmPassword] = useWatch({ + control, + name: ['nickname', 'email', 'password', 'confirmPassword'], + }); - const password = watch('password'); + const [debouncedNickname, cancelDebouncedNickname] = useDebouncedValue(nickname, 1000); + const [debouncedEmail, cancelDebouncedEmail] = useDebouncedValue(email, 1000); + const [debouncedPassword, cancelDebouncedPassword] = useDebouncedValue(password, 1000); + const [debouncedConfirmPassword, cancelDebouncedConfirmPassword] = useDebouncedValue( + confirmPassword, + 1000, + ); + + useEffect(() => { + if (debouncedNickname) trigger('nickname'); + }, [nickname, trigger]); + + useEffect(() => { + if (debouncedEmail) trigger('email'); + }, [email, trigger]); useEffect(() => { - if (password) { + if (debouncedPassword) { + trigger('password'); trigger('confirmPassword'); } }, [password, trigger]); + useEffect(() => { + if (debouncedConfirmPassword) trigger('confirmPassword'); + }, [confirmPassword, trigger]); + + const handleFormSubmit = handleSubmit(async (data) => { + onSubmit(data); + cancelDebouncedNickname(); + cancelDebouncedEmail(); + cancelDebouncedPassword(); + cancelDebouncedConfirmPassword(); + }); + return ( - + trigger('nickname'), - onChange: async () => debouncedTrigger('nickname'), }), }} error={errors.nickname?.message} @@ -65,7 +92,6 @@ export default function SignupForm({ formMethods, onSubmit }: SignupFormProps) { message: '이메일 형식으로 입력해주세요', }, onBlur: () => trigger('email'), - onChange: async () => debouncedTrigger('email'), }), }} error={errors.email?.message} @@ -84,7 +110,6 @@ export default function SignupForm({ formMethods, onSubmit }: SignupFormProps) { message: '비밀번호가 8자 이상이 되도록 해주세요.', }, onBlur: () => trigger('password'), - onChange: async () => debouncedTrigger('password'), }), }} error={errors.password?.message} @@ -100,7 +125,6 @@ export default function SignupForm({ formMethods, onSubmit }: SignupFormProps) { required: '비밀번호를 다시 입력해주세요', validate: (value) => value === password || '비밀번호가 일치하지 않습니다.', onBlur: () => trigger('confirmPassword'), - onChange: async () => debouncedTrigger('confirmPassword'), }), }} error={errors.confirmPassword?.message} @@ -109,8 +133,11 @@ export default function SignupForm({ formMethods, onSubmit }: SignupFormProps) { />
- {/* TODO: button 바꾸기 */} -
diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx index e36deae8..9760dc24 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/(auth)/signup/page.tsx @@ -1,24 +1,31 @@ 'use client'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import Link from 'next/link'; -import { useRouter } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import { usePostSignupQuery } from '@/src/_queries/auth/signup-queries'; import SignupForm, { SignupFormValues } from './_component/signup-form'; export default function LoginPage() { + const [redirect, setRedirect] = useState('/'); + const searchParams = useSearchParams(); const router = useRouter(); const formMethods = useForm(); const { setError } = formMethods; const { mutate: postSignup } = usePostSignupQuery(); + useEffect(() => { + const redirectParam = searchParams.get('redirect'); + if (redirectParam) setRedirect(redirectParam); + }, [searchParams]); + const handleSubmit = async (data: SignupFormValues) => { const { confirmPassword, ...requestData } = data; postSignup(requestData, { onSuccess: () => { - router.push('/'); + router.push(redirect); }, onError: (error) => { if (error.status === 400) { @@ -39,7 +46,10 @@ export default function LoginPage() {
이미 회원이신가요?
- + 로그인
diff --git a/src/app/(crew)/_components/hero/hero-crew.tsx b/src/app/(crew)/_components/hero/hero-crew.tsx index 38a24889..dd7ac7fe 100644 --- a/src/app/(crew)/_components/hero/hero-crew.tsx +++ b/src/app/(crew)/_components/hero/hero-crew.tsx @@ -1,10 +1,12 @@ import Image from 'next/image'; import Link from 'next/link'; -import { useAuthStore } from '@/src/store/use-auth-store'; +import { usePathname } from 'next/navigation'; +import { useAuth } from '@/src/hooks/use-auth'; import ImgHeroCrew from '@/public/assets/icons/ic-dumbbell.svg'; export default function HeroCrew() { - const { isAuth } = useAuthStore(); + const currentPath = usePathname(); + const { isAuth } = useAuth(); return (
@@ -21,7 +23,7 @@ export default function HeroCrew() {

크루 만들기 diff --git a/src/app/(crew)/crew/detail/[id]/_components/create-gathering/index.tsx b/src/app/(crew)/crew/detail/[id]/_components/create-gathering/index.tsx index 6446ed09..e6c6881c 100644 --- a/src/app/(crew)/crew/detail/[id]/_components/create-gathering/index.tsx +++ b/src/app/(crew)/crew/detail/[id]/_components/create-gathering/index.tsx @@ -2,13 +2,13 @@ import { useRouter } from 'next/navigation'; import { useDisclosure } from '@mantine/hooks'; -import { useAuthStore } from '@/src/store/use-auth-store'; +import { useAuth } from '@/src/hooks/use-auth'; import CreateGatheringModalContainer from '@/src/app/(crew)/crew/detail/[id]/_components/create-gathering/create-gathering-modal/container'; import Button from '@/src/components/common/input/button'; import { CreateGatheringFormTypes } from '@/src/types/gathering-data'; export default function CreateGathering({ crewId }: { crewId: number }) { - const { isAuth } = useAuthStore(); + const { isAuth } = useAuth(); const router = useRouter(); const [opened, { open, close }] = useDisclosure(false); diff --git a/src/app/(crew)/crew/detail/[id]/_components/detail-crew-container.tsx b/src/app/(crew)/crew/detail/[id]/_components/detail-crew-container.tsx index a73f9249..41cf87a9 100644 --- a/src/app/(crew)/crew/detail/[id]/_components/detail-crew-container.tsx +++ b/src/app/(crew)/crew/detail/[id]/_components/detail-crew-container.tsx @@ -5,8 +5,8 @@ import { toast } from 'react-toastify'; import { useRouter } from 'next/navigation'; import { useDisclosure } from '@mantine/hooks'; import { cancelCrew, joinCrew, leaveCrew } from '@/src/_apis/crew/crew-detail-apis'; +import { useUser } from '@/src/_queries/auth/user-queries'; import { useGetCrewDetailQuery } from '@/src/_queries/crew/crew-detail-queries'; -import { useAuthStore } from '@/src/store/use-auth-store'; import { ApiError } from '@/src/utils/api'; import ConfirmCancelModal from '@/src/components/common/modal/confirm-cancel-modal'; import { User } from '@/src/types/auth'; @@ -24,7 +24,7 @@ export default function DetailCrew({ id }: DetailCrewContainerProps) { useDisclosure(); const router = useRouter(); - const { user } = useAuthStore(); + const { data: user } = useUser(); const isDataWrappedUser = (value: unknown): value is { data: User } => { return typeof value === 'object' && value !== null && 'data' in value; diff --git a/src/app/(crew)/my-page/_components/profile-card/container.tsx b/src/app/(crew)/my-page/_components/profile-card/container.tsx index e4747c23..8b65ece3 100644 --- a/src/app/(crew)/my-page/_components/profile-card/container.tsx +++ b/src/app/(crew)/my-page/_components/profile-card/container.tsx @@ -8,20 +8,23 @@ import { resetUserProfileImage, updateUserProfile, } from '@/src/_apis/auth/user-apis'; -import { useAuthStore } from '@/src/store/use-auth-store'; -import { User } from '@/src/types/auth'; +import { useUser } from '@/src/_queries/auth/user-queries'; +import { useAuth } from '@/src/hooks/use-auth'; import ProfileCardPresenter from './presenter'; export default function ProfileCard() { const router = useRouter(); - const { isAuth, rehydrated, setUser } = useAuthStore(); - const [user, setLocalUser] = useState(null); + const { isAuth } = useAuth(); + const { data: user } = useUser(); + + // const { rehydrated, setUser } = useAuthStore(); + // const [user, setLocalUser] = useState(null); const [isLoading, setIsLoading] = useState(true); const [profileImageUrl, setProfileImageUrl] = useState(''); useEffect(() => { const checkAuthAndLoadUser = async () => { - if (!rehydrated) return; // 상태 복원이 완료되지 않았으면 대기 + // if (!rehydrated) return; // 상태 복원이 완료되지 않았으면 대기 if (!isAuth) { router.push('/login'); // 인증되지 않은 경우 리디렉션 @@ -32,8 +35,8 @@ export default function ProfileCard() { try { const updatedUser = await fetchUpdatedUser(); - setLocalUser(updatedUser); - setUser(updatedUser); + // setLocalUser(updatedUser); + // setUser(updatedUser); setProfileImageUrl(updatedUser.profileImageUrl); } catch { toast.error('유저 정보를 가져오는 데 실패했습니다.'); @@ -43,9 +46,10 @@ export default function ProfileCard() { }; checkAuthAndLoadUser(); - }, [isAuth, rehydrated, router, setUser]); + // }, [isAuth, rehydrated, router, setUser]); + }, [isAuth, router]); - if (!rehydrated) return null; + // if (!rehydrated) return null; if (!isAuth) return null; if (isLoading) return
로딩 중...
; if (!user) return
유저 정보를 불러오지 못했습니다.
; @@ -71,7 +75,7 @@ export default function ProfileCard() { const updatedUser = await fetchUpdatedUser(); const newProfileImageUrl = `${updatedUser.profileImageUrl}?timestamp=${new Date().getTime()}`; setProfileImageUrl(newProfileImageUrl); - setUser({ ...updatedUser, profileImageUrl: newProfileImageUrl }); + // setUser({ ...updatedUser, profileImageUrl: newProfileImageUrl }); } catch (error) { toast.error('파일 업로드에 실패했습니다.'); } @@ -85,8 +89,8 @@ export default function ProfileCard() { await resetUserProfileImage(); const updatedUser = await fetchUpdatedUser(); setProfileImageUrl(''); // 초기화된 이미지 반영 - setLocalUser(updatedUser); - setUser(updatedUser); + // setLocalUser(updatedUser); + // setUser(updatedUser); toast.success('프로필 이미지가 초기화되었습니다.'); } catch (error) { toast.error('프로필 이미지 초기화에 실패했습니다.'); diff --git a/src/components/client-provider.tsx b/src/components/client-provider.tsx index 91a397cc..80d3d59c 100644 --- a/src/components/client-provider.tsx +++ b/src/components/client-provider.tsx @@ -1,19 +1,45 @@ 'use client'; -import { ReactNode } from 'react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactNode, useState } from 'react'; +import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental'; +import { reissue } from '../_apis/auth/reissue-apis'; +import { useAuthStore } from '../store/use-auth-store'; export default function ClientProvider({ children }: { children: ReactNode }) { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - // 서버 렌더링 시 데이터를 새로 고침하지 않도록 설정 - staleTime: 60 * 1000, // 1분 동안 데이터 유지 - }, - }, - }); + const setToken = useAuthStore((state) => state.setToken); + + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + // 서버 렌더링 시 데이터를 새로 고침하지 않도록 설정 + staleTime: 60 * 1000, // 1분 동안 데이터 유지 + retry: (failureCount, error) => { + if (error.message === '토큰이 올바르지 않습니다.') { + return false; + } + return failureCount <= 1; + }, + }, + }, + // accessToken 에러 -> reissue() -> refetch() + queryCache: new QueryCache({ + onError: async (error, query) => { + if (error.message === '토큰이 올바르지 않습니다.') { + const { token } = await reissue(); + if (token) setToken(token.replace(/^Bearer\s/, '')); + await Promise.all([ + queryClient.refetchQueries({ queryKey: query.queryKey }), + queryClient.refetchQueries({ queryKey: ['user'] }), + ]); + } + }, + }), + }), + ); return ( diff --git a/src/components/common/header/container.tsx b/src/components/common/header/container.tsx index 00ca1dcd..7041776b 100644 --- a/src/components/common/header/container.tsx +++ b/src/components/common/header/container.tsx @@ -1,7 +1,8 @@ 'use client'; -import { useRouter } from 'next/navigation'; -import { useAuthStore } from '@/src/store/use-auth-store'; +import { useUser } from '@/src/_queries/auth/user-queries'; +import { useAuth } from '@/src/hooks/use-auth'; +import { useLogout } from '@/src/hooks/use-logout'; import HeaderPresenter from '@/src/components/common/header/presenter'; /** @@ -12,14 +13,9 @@ import HeaderPresenter from '@/src/components/common/header/presenter'; */ export default function Header() { - const { isAuth, user, logout } = useAuthStore(); // user 정보 가져오기 - - const router = useRouter(); - - const handleLogout = () => { - logout(); - router.push('/'); - }; + const { handleLogout } = useLogout(); + const { isAuth } = useAuth(); + const { data: user } = useUser(); return (
diff --git a/src/components/common/header/header.stories.tsx b/src/components/common/header/header.stories.tsx index 143c1e5f..29e3ed69 100644 --- a/src/components/common/header/header.stories.tsx +++ b/src/components/common/header/header.stories.tsx @@ -1,5 +1,7 @@ +import { useState } from 'react'; import type { Meta, StoryFn } from '@storybook/react'; -import { useAuthStore } from '@/src/store/use-auth-store'; +import { QueryClient, QueryClientProvider, useQueryClient } from '@tanstack/react-query'; +import { useAuth } from '@/src/hooks/use-auth'; import Header from '@/src/components/common/header/container'; const meta: Meta = { @@ -15,32 +17,10 @@ const meta: Meta = { export default meta; function Template() { - const { isAuth, login, logout, setUser } = useAuthStore(); - const testToken = 'test token'; - const testUser = { - id: 1, - nickname: '크루크루', - email: 'john@example.com', - profileImageUrl: 'https://i.pinimg.com/736x/3f/e4/f4/3fe4f4f3aee36ec57aa072cce2e016b3.jpg', - }; - - const toggleAuth = () => { - if (isAuth) { - logout(); - } else { - login(testToken); - setUser(testUser); - } - }; - return ( -
+
- -
+ ); } diff --git a/src/components/common/header/presenter.tsx b/src/components/common/header/presenter.tsx index 4212fe81..37181812 100644 --- a/src/components/common/header/presenter.tsx +++ b/src/components/common/header/presenter.tsx @@ -43,8 +43,10 @@ export default function HeaderPresenter({ {links.map(({ href, label }) => ( {label} @@ -76,7 +78,6 @@ export default function HeaderPresenter({ )}
- {/* */} ); } diff --git a/src/hooks/use-auth.ts b/src/hooks/use-auth.ts new file mode 100644 index 00000000..7073f4e8 --- /dev/null +++ b/src/hooks/use-auth.ts @@ -0,0 +1,7 @@ +import { useUser } from '../_queries/auth/user-queries'; + +export function useAuth() { + const { data: user } = useUser(); + const isAuth = !!user; + return { isAuth }; +} diff --git a/src/_queries/auth/use-handle-auth-success.ts b/src/hooks/use-handle-auth-success.ts similarity index 54% rename from src/_queries/auth/use-handle-auth-success.ts rename to src/hooks/use-handle-auth-success.ts index 1352948f..e2702e8c 100644 --- a/src/_queries/auth/use-handle-auth-success.ts +++ b/src/hooks/use-handle-auth-success.ts @@ -1,11 +1,7 @@ -import { useQueryClient } from '@tanstack/react-query'; import { useAuthStore } from '@/src/store/use-auth-store'; -import { User } from '@/src/types/auth'; -import { getUserQuery } from './user-apis'; export function useHandleAuthSuccess() { - const queryClient = useQueryClient(); - const { login, setUser } = useAuthStore(); + const setToken = useAuthStore((state) => state.setToken); return async function handleAuthSuccess(token: string | null) { if (!token) { @@ -14,9 +10,7 @@ export function useHandleAuthSuccess() { try { const accessToken = token.replace(/^Bearer\s/, ''); - login(accessToken); - const user: User = await queryClient.fetchQuery(getUserQuery()); - setUser(user); + setToken(accessToken); } catch (error) { throw new Error('사용자 상태 업데이트 실패'); } diff --git a/src/hooks/use-logout.ts b/src/hooks/use-logout.ts new file mode 100644 index 00000000..9a40d9a5 --- /dev/null +++ b/src/hooks/use-logout.ts @@ -0,0 +1,24 @@ +import { useRouter } from 'next/navigation'; +import { useQueryClient } from '@tanstack/react-query'; +import { usePostLogoutQuery } from '../_queries/auth/logout-queries'; +import { useAuthStore } from '../store/use-auth-store'; + +export function useLogout() { + const { mutate: postLogout } = usePostLogoutQuery(); + const clearToken = useAuthStore((state) => state.clearToken); + const queryClient = useQueryClient(); + + const router = useRouter(); + + return { + handleLogout: () => { + postLogout(undefined, { + onSuccess: () => { + router.push('/'); + clearToken(); + queryClient.removeQueries({ queryKey: ['user'] }); + }, + }); + }, + }; +} diff --git a/src/store/use-auth-store.tsx b/src/store/use-auth-store.tsx index 67a2881c..ff3feb50 100644 --- a/src/store/use-auth-store.tsx +++ b/src/store/use-auth-store.tsx @@ -1,47 +1,29 @@ -import { create } from 'zustand'; +import { createStore, useStore } from 'zustand'; import { createJSONStorage, persist } from 'zustand/middleware'; -import { User } from '@/src/types/auth'; interface AuthState { - isAuth: boolean; - user: User | null; token: string | null; - rehydrated: boolean; // 상태 복원 여부 - login: (token: string) => void; - logout: () => void; - setUser: (user: User) => void; + setToken: (token: string) => void; + clearToken: () => void; } -export const useAuthStore = create()( +export const authStore = createStore()( persist( (set) => ({ - isAuth: false, - user: null, token: null, - rehydrated: false, - login: (token) => set({ isAuth: true, token }), - logout: () => - set({ - isAuth: false, - user: null, - token: null, - }), - setUser: (user: User) => set({ user, isAuth: true }), + setToken: (token) => set({ token }), + clearToken: () => set({ token: null }), }), { name: 'auth-storage', storage: createJSONStorage(() => sessionStorage), - onRehydrateStorage: () => (state) => { - if (state) { - // eslint-disable-next-line no-param-reassign - state.rehydrated = true; // 상태 복원 완료 - } - }, partialize: (state) => ({ - isAuth: state.isAuth, token: state.token, - user: state.user, }), }, ), ); + +export function useAuthStore(selector: (state: AuthState) => T) { + return useStore(authStore, selector); +} diff --git a/src/utils/api.ts b/src/utils/api.ts index f47c8491..a76dfd2f 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -1,4 +1,4 @@ -import { useAuthStore } from '@/src/store/use-auth-store'; +import { authStore } from '@/src/store/use-auth-store'; export class ApiError extends Error { detail: { validationErrors: Record } = { validationErrors: {} }; @@ -24,7 +24,7 @@ export async function fetchApi( const controller = new AbortController(); const { signal } = controller; const id = setTimeout(() => controller.abort(), timeout); - const { token } = useAuthStore.getState(); + const { token } = authStore.getState(); const fetchOptions: RequestInit = { ...options, signal,