diff --git a/.gitignore b/.gitignore index acf0cc14..83e921ca 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ # next.js /.next/ +.next/ /out/ # production diff --git a/README.md b/README.md index 5340075c..4f21919d 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,6 @@ > **떠날 준비, 지금 바로, RoamReady – 즉흥의 설렘을 예약하세요.** - - **롬레디 (RoamReady)** 는 Roam과 Ready의 결합어로, 언제든지 떠날 준비가 된 여행자와 체험가를 위한 액티비티 예약 플랫폼입니다.
즉흥적인 여행, 자유로운 탐험, 감각적인 경험을 추구하는 당신을 위해 감성적이면서도 실용적인 디자인, 빠르고 유연한 예약 흐름, 그리고 글로벌 확장을 고려한 구조로 설계했습니다. @@ -15,6 +13,67 @@ 🎬 [바로 보기(YouTube에서 재생됩니다)](https://www.youtube.com/watch?v=ikNrX0suphQ)
+
+Preview (GIF) + +#### 회원가입/로그인 + +![auth1.gif](./gifs/auth1.gif) +![auth2.gif](./gifs/auth2.gif) +![auth3.gif](./gifs/auth3.gif) + +#### OAuth(kakao) + +![kakao.gif](./gifs/kakao.gif) + +--- + +#### 메인페이지 캐러셀 + +![main-carousel.gif](./gifs/main-carousel.gif) + +#### 메인페이지 리스트 + +![main-list-filter.gif](./gifs/main-list-filter.gif) +![main-list-pagination.gif](./gifs/main-list-pagination.gif) + +#### 메인페이지 검색 + +![main-search.gif](./gifs/main-search.gif) + +--- + +#### 마이페이지 내 정보 수정 + +![my-info.gif](./gifs/my-info.gif) + +#### 마이페이지 예약 관리 + +![my-reservation.gif](./gifs/my-reservation.gif) + +#### 마이페이지 내 체험 관리 + +![my-activity-edit-delete.gif](./gifs/my-activity-edit-delete.gif) + +#### 마이페이지 내 예약 현황 + +![my-reservation-list.gif](./gifs/my-reservation-list.gif) + +--- + +#### 상세페이지 + +![detail.gif](./gifs/detail.gif) + +--- + +#### 체험 등록 + +![create-activity1.gif](./gifs/create-activity1.gif) +![create-activity2.gif](./gifs/create-activity2.gif) + +
+ --- @@ -29,10 +88,10 @@ 시은 @@ -47,7 +106,7 @@ - + diff --git a/gifs/auth1.gif b/gifs/auth1.gif new file mode 100644 index 00000000..a5278479 Binary files /dev/null and b/gifs/auth1.gif differ diff --git a/gifs/auth2.gif b/gifs/auth2.gif new file mode 100644 index 00000000..a2a29ae3 Binary files /dev/null and b/gifs/auth2.gif differ diff --git a/gifs/auth3.gif b/gifs/auth3.gif new file mode 100644 index 00000000..b71371e3 Binary files /dev/null and b/gifs/auth3.gif differ diff --git a/gifs/create-activity1.gif b/gifs/create-activity1.gif new file mode 100644 index 00000000..a1682e86 Binary files /dev/null and b/gifs/create-activity1.gif differ diff --git a/gifs/create-activity2.gif b/gifs/create-activity2.gif new file mode 100644 index 00000000..cd273063 Binary files /dev/null and b/gifs/create-activity2.gif differ diff --git a/gifs/detail.gif b/gifs/detail.gif new file mode 100644 index 00000000..58b46364 Binary files /dev/null and b/gifs/detail.gif differ diff --git a/gifs/kakao.gif b/gifs/kakao.gif new file mode 100644 index 00000000..79005cd6 Binary files /dev/null and b/gifs/kakao.gif differ diff --git a/gifs/main-carousel.gif b/gifs/main-carousel.gif new file mode 100644 index 00000000..4afcd039 Binary files /dev/null and b/gifs/main-carousel.gif differ diff --git a/gifs/main-list-filter.gif b/gifs/main-list-filter.gif new file mode 100644 index 00000000..1877896d Binary files /dev/null and b/gifs/main-list-filter.gif differ diff --git a/gifs/main-list-pagination.gif b/gifs/main-list-pagination.gif new file mode 100644 index 00000000..f2e21bcc Binary files /dev/null and b/gifs/main-list-pagination.gif differ diff --git a/gifs/main-search.gif b/gifs/main-search.gif new file mode 100644 index 00000000..a6fa6b7e Binary files /dev/null and b/gifs/main-search.gif differ diff --git a/gifs/my-activity-edit-delete.gif b/gifs/my-activity-edit-delete.gif new file mode 100644 index 00000000..17d9ef03 Binary files /dev/null and b/gifs/my-activity-edit-delete.gif differ diff --git a/gifs/my-info.gif b/gifs/my-info.gif new file mode 100644 index 00000000..3d6658e0 Binary files /dev/null and b/gifs/my-info.gif differ diff --git a/gifs/my-reservation-list.gif b/gifs/my-reservation-list.gif new file mode 100644 index 00000000..3c3c9d6b Binary files /dev/null and b/gifs/my-reservation-list.gif differ diff --git a/gifs/my-reservation.gif b/gifs/my-reservation.gif new file mode 100644 index 00000000..f313ba79 Binary files /dev/null and b/gifs/my-reservation.gif differ diff --git a/next.config.ts b/next.config.ts index 41ea4601..f1f60742 100644 --- a/next.config.ts +++ b/next.config.ts @@ -26,8 +26,7 @@ const nextConfig: NextConfig = { }, ], - // unoptimized: true, - + unoptimized: true, }, }; diff --git a/src/app/_components/AuthInitializer.tsx b/src/app/_components/AuthInitializer.tsx new file mode 100644 index 00000000..6aaf306a --- /dev/null +++ b/src/app/_components/AuthInitializer.tsx @@ -0,0 +1,16 @@ +'use client'; + +import { useUser } from '@/domain/Auth/hooks/useUser'; + +/** + * @component AuthInitializer + * @description + * 앱이 처음 로드될 때 `useUser` 훅을 호출하여 + * 서버의 실제 인증 상태와 클라이언트의 UI 상태를 동기화하는 역할을 합니다. + * 이 컴포넌트는 UI를 렌더링하지 않습니다. + */ +export default function AuthInitializer() { + useUser(); + + return null; +} diff --git a/src/app/_components/ClientProvider.tsx b/src/app/_components/ClientProvider.tsx index 0c034ca2..9b80046c 100644 --- a/src/app/_components/ClientProvider.tsx +++ b/src/app/_components/ClientProvider.tsx @@ -3,27 +3,30 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { ReactNode } from 'react'; +import AuthInitializer from '@/app/_components/AuthInitializer'; import ToastContainer from '@/shared/components/ui/toast/ToastContainer'; import { queryClient } from '@/shared/libs/queryClient'; /** - * @component ClientProviders + * @component ClientProvider * @description - * 클라이언트 측에서 필요한 전역 프로바이더들을 통합하여 제공하는 컴포넌트입니다. - * 이 컴포넌트는 Tanstack Query의 `QueryClientProvider`를 설정하여 애플리케이션 전반에서 데이터 페칭 및 캐싱을 관리하고, - * 전역 토스트 알림을 위한 `ToastContainer`를 렌더링합니다. + * 클라이언트 측에서 필요한 전역 프로바이더들을 통합하여 제공하는 핵심 컴포넌트입니다. + * 이 컴포넌트는 다음의 세 가지 주요 역할을 수행합니다: * - * 이 컴포넌트는 Next.js의 'use client' 지시어가 적용되어 클라이언트 사이드에서만 동작하며, - * 애플리케이션의 루트 레이아웃에서 사용되어야 합니다. + * 1. **인증 상태 초기화**: 앱이 시작될 때 ``를 통해 실제 서버의 인증 상태와 클라이언트 UI 상태를 동기화하여, "UI는 로그인 상태인데 실제로는 토큰이 만료된" 상태 불일치 문제를 해결합니다. + * 2. **서버 상태 관리**: Tanstack Query의 `QueryClientProvider`를 설정하여 애플리케이션 전반에서 데이터 페칭 및 캐싱을 관리합니다. + * 3. **전역 알림**: 전역 토스트 알림을 위한 ``를 렌더링합니다. * - * @param {ClientProvidersProps} props - 컴포넌트의 props입니다. - * @param {ReactNode} props.children - 이 프로바이더 내부에 렌더링될 자식 요소입니다. + * 이 컴포넌트는 Next.js의 'use client' 지시어가 적용되어 클라이언트 사이드에서만 동작하며, 애플리케이션의 루트 레이아웃에서 사용되어야 합니다. * - * @returns {JSX.Element} QueryClientProvider와 ToastContainer로 래핑된 자식 요소 React 요소입니다. + * @param {object} props - 컴포넌트의 props입니다. + * @param {ReactNode} props.children - 프로바이더 내부에 렌더링될 자식 요소입니다. + * @returns {JSX.Element} 각종 프로바이더로 래핑된 자식 요소입니다. */ -export default function ClientProviders({ children }: { children: ReactNode }) { +export default function ClientProvider({ children }: { children: ReactNode }) { return ( + {children} diff --git a/src/app/api/auth/kakao/signin/route.ts b/src/app/api/auth/kakao/signin/route.ts index 27bceb71..57166550 100644 --- a/src/app/api/auth/kakao/signin/route.ts +++ b/src/app/api/auth/kakao/signin/route.ts @@ -105,7 +105,7 @@ export async function GET(request: NextRequest) { redirectToTransition.searchParams.set('status', 'need-signup'); redirectToTransition.searchParams.set( 'message', - error instanceof Error ? error.message : '회원가입 먼저 해주세요.', + error instanceof Error ? error.message : '등록되지 않은 사용자입니다.', ); return NextResponse.redirect(redirectToTransition); } @@ -117,6 +117,7 @@ export async function GET(request: NextRequest) { if (error instanceof Error) { defaultErrorUrl.searchParams.append('message', error.message); } + return NextResponse.redirect(defaultErrorUrl); } } diff --git a/src/app/api/auth/kakao/signup/route.ts b/src/app/api/auth/kakao/signup/route.ts index ad044b05..77632ab6 100644 --- a/src/app/api/auth/kakao/signup/route.ts +++ b/src/app/api/auth/kakao/signup/route.ts @@ -61,7 +61,7 @@ async function handleOauthSignUp( * 카카오 OAuth 회원가입 콜백을 처리하는 라우트입니다. * * 1. 인가 코드(code)를 쿼리에서 추출 - * 2. 자동 생성된 랜덤 닉네임으로 회원가입 시도 + * 2. 사용자로부터 받은 닉네임으로 회원가입 시도 * 3. 성공 시 토큰을 쿠키에 저장하고 `/activities`로 이동 * 4. 실패 시 상태에 따라 리디렉션 분기 * @@ -75,6 +75,7 @@ async function handleOauthSignUp( export async function GET(request: NextRequest) { try { const code = request.nextUrl.searchParams.get('code'); + const state = request.nextUrl.searchParams.get('state'); if (!code) { throw Object.assign(new Error('카카오 인증 코드가 없습니다.'), { @@ -82,8 +83,17 @@ export async function GET(request: NextRequest) { }); } - const arbitraryNickname = `K_${crypto.randomUUID().replace(/-/g, '').slice(0, 7)}`; - const responseData = await handleOauthSignUp(code, arbitraryNickname); + const nickname = state ? decodeURIComponent(state) : null; + + if (!nickname) { + throw Object.assign( + new Error('회원가입에 필요한 닉네임 정보가 없습니다.'), + { status: 400 }, + ); + } + + // const arbitraryNickname = `K_${crypto.randomUUID().replace(/-/g, '').slice(0, 7)}`; + const responseData = await handleOauthSignUp(code, nickname); const response = NextResponse.redirect( new URL(ROUTES.ACTIVITIES.ROOT, request.url), diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 40083eff..1d68487d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,7 +4,7 @@ import type { Metadata } from 'next'; import localFont from 'next/font/local'; import { ReactNode } from 'react'; -import ClientProviders from './_components/ClientProvider'; +import ClientProvider from './_components/ClientProvider'; export const metadata: Metadata = { title: { @@ -30,7 +30,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
- {children} + {children} ); diff --git a/src/domain/Auth/components/KakaoTransition.tsx b/src/domain/Auth/components/KakaoTransition.tsx index 4cf09ef5..cde0ddf1 100644 --- a/src/domain/Auth/components/KakaoTransition.tsx +++ b/src/domain/Auth/components/KakaoTransition.tsx @@ -1,47 +1,73 @@ 'use client'; +import { zodResolver } from '@hookform/resolvers/zod'; import Image from 'next/image'; import { useSearchParams } from 'next/navigation'; +import { FormProvider, useForm } from 'react-hook-form'; +import type { TransitionNicknameFormValues } from '@/domain/Auth/schemas/request'; +import { transitionNicknameSchema } from '@/domain/Auth/schemas/request'; import LogoSymbol from '@/shared/assets/logos/LogoSymbol'; import Button from '@/shared/components/Button'; +import Input from '@/shared/components/ui/input'; +import { cn } from '@/shared/libs/cn'; const KAKAO_REDIRECT_URI = process.env.NEXT_PUBLIC_KAKAO_SIGNUP_REDIRECT_URI!; const KAKAO_CLIENT_ID = process.env.NEXT_PUBLIC_KAKAO_REST_API_KEY!; /** - * @component KakaoTransitionPage + * @component KakaoTransition * @description - * 카카오 OAuth 인증 흐름 중, 사용자가 로그인 또는 회원가입 중간에 분기되는 전환 페이지입니다. + * 이 컴포넌트는 카카오 OAuth 인증 흐름 중, 사용자가 로그인 또는 회원가입 과정에서 특정 상황에 따라 분기될 때 표시되는 전환 페이지입니다. * - * - 백엔드 응답에서 `403`, `404`, `409`, `400` 등의 상태 코드가 발생할 경우, - * 서버는 클라이언트를 `/kakao/transition?status=...` 주소로 리디렉션합니다. + * 이 페이지는 URL 쿼리 파라미터로 전달된 `status`와 `message`를 읽어, + * 상황에 맞는 안내 메시지를 표시하고 사용자를 다시 카카오 인증 페이지로 리디렉션하는 버튼을 제공합니다. * - * - 해당 페이지는 쿼리 파라미터로 전달된 `status`, `message`를 읽고, - * 상황에 따라 안내 메시지와 함께 다시 카카오 인증 페이지로 유도합니다. + * @param {object} props - 이 컴포넌트는 props를 직접 사용하지 않으며, URL 쿼리 파라미터에 의존합니다. * * @example - * ``` - * /kakao/transition?status=need-signup&message=회원가입 먼저 해주세요. + * 회원가입이 필요한 사용자를 위한 URL + * /kakao/transition?status=need-signup&message=회원가입 먼저 진행해주세요. * - * ``` + * 일반적인 오류가 발생했을 때의 URL + * /kakao/transition?status=some-error&message=처리 중 문제가 발생했습니다. * * ### 쿼리 파라미터 - * - `status`: 분기 상태 (`need-signup`) - * - `message`: 사용자에게 표시할 메시지 + * @param {string} status - 필요한 조치를 나타내는 상태 코드. + * - `need-signup`: 사용자가 아직 회원이 아니므로 회원가입이 필요합니다. + * - 그 외의 상태 코드(예: 서버 오류)는 일반적인 오류 메시지를 표시합니다. + * @param {string} message - 사용자에게 보여줄 친화적인 메시지. * * ### 동작 흐름 - * 1. `status`가 `need-signup`이면 "회원 가입 먼저 진행해주세요."라는 안내 문구가 표시됩니다. - * 2. 버튼을 누르면 카카오 인증 페이지로 다시 리디렉션되어 새로운 인가 코드를 받게 됩니다. + * 1. 컴포넌트는 URL 쿼리 파라미터에서 `status`와 `message`를 읽어옵니다. + * 2. `status`가 'need-signup'인 경우, "회원 가입 먼저 진행해주세요."라는 메시지와 함께 사용자가 닉네임을 입력할 수 있는 입력 필드를 표시합니다. + * 3. 사용자는 닉네임을 입력하고 "계속하기" 버튼을 클릭합니다. + * 4. `handleRedirectToKakao` 함수가 실행되어 새로운 카카오 인가 URL을 생성합니다. + * 5. 입력된 닉네임은 URL 인코딩되어 새로운 인가 URL의 `state` 파라미터로 포함됩니다. + * 6. 사용자는 카카오 인증 페이지로 리디렉션되어 새로운 인가 코드를 받게 되며, 서버는 이 코드를 통해 회원가입 절차를 진행합니다. */ export default function KakaoTransition() { const searchParams = useSearchParams(); - const status = searchParams.get('status'); const message = searchParams.get('message'); + const methods = useForm({ + mode: 'onChange', + resolver: zodResolver(transitionNicknameSchema), + defaultValues: { + nickname: '', + }, + }); + + const { getValues, formState } = methods; + const handleRedirectToKakao = () => { - const kakaoAuthUrl = `https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=${KAKAO_CLIENT_ID}&redirect_uri=${KAKAO_REDIRECT_URI}`; + const nickname = getValues('nickname'); + + // 닉네임을 URL에 포함될 수 있도록 인코딩 + const encodedNickname = encodeURIComponent(nickname); + + const kakaoAuthUrl = `https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=${KAKAO_CLIENT_ID}&redirect_uri=${KAKAO_REDIRECT_URI}&state=${encodedNickname}`; window.location.href = kakaoAuthUrl; }; @@ -52,27 +78,41 @@ export default function KakaoTransition() { : '처리 중 문제가 발생했습니다.'; return ( -
- -

{title}

-

{message ?? '아래 버튼을 눌러 회원가입하세요.'}

- - -
+ + {notAMember && ( + + + + + )} + + + + ); } diff --git a/src/domain/Auth/hooks/useSigninMutation.ts b/src/domain/Auth/hooks/useSigninMutation.ts index a54c791d..d79899c3 100644 --- a/src/domain/Auth/hooks/useSigninMutation.ts +++ b/src/domain/Auth/hooks/useSigninMutation.ts @@ -2,11 +2,10 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { HTTPError } from 'ky'; import { useRouter } from 'next/navigation'; -import type { SigninResponse } from '@/domain/Auth/schemas/response'; import { signin } from '@/domain/Auth/services'; import { ROUTES } from '@/shared/constants/routes'; import { useToast } from '@/shared/hooks/useToast'; -import { useRoamReadyStore } from '@/shared/store'; +// import { useRoamReadyStore } from '@/shared/store'; /** * @function useSigninMutation @@ -33,15 +32,16 @@ import { useRoamReadyStore } from '@/shared/store'; */ export const useSigninMutation = () => { const router = useRouter(); - const setUser = useRoamReadyStore((state) => state.setUser); + // const setUser = useRoamReadyStore((state) => state.setUser); const queryClient = useQueryClient(); const { showError } = useToast(); return useMutation({ mutationFn: signin, - onSuccess: (data: SigninResponse) => { + onSuccess: () => { + // onSuccess: (data: SigninResponse) => { sessionStorage.removeItem('signup-form'); - setUser(data.user); + // setUser(data.user); queryClient.invalidateQueries({ queryKey: ['user', 'me'] }); // showSuccess('로그인 되었습니다. 환영합니다!'); router.push(ROUTES.ACTIVITIES.ROOT); diff --git a/src/domain/Auth/hooks/useSignupMutation.ts b/src/domain/Auth/hooks/useSignupMutation.ts index c14d3d33..36527ece 100644 --- a/src/domain/Auth/hooks/useSignupMutation.ts +++ b/src/domain/Auth/hooks/useSignupMutation.ts @@ -8,7 +8,7 @@ import type { SignupResponse } from '@/domain/Auth/schemas/response'; import { signup } from '@/domain/Auth/services'; import { ROUTES } from '@/shared/constants/routes'; import { useToast } from '@/shared/hooks/useToast'; -import { useRoamReadyStore } from '@/shared/store'; +// import { useRoamReadyStore } from '@/shared/store'; /** * @function useSignupMutation @@ -38,7 +38,7 @@ export const useSignupMutation = ( form: UseFormReturn, ) => { const router = useRouter(); - const setUser = useRoamReadyStore((state) => state.setUser); + // const setUser = useRoamReadyStore((state) => state.setUser); const queryClient = useQueryClient(); const { showSuccess, showError } = useToast(); @@ -59,7 +59,7 @@ export const useSignupMutation = ( return; } - setUser(data); + // setUser(data); queryClient.invalidateQueries({ queryKey: ['user', 'me'] }); // showSuccess('회원가입이 완료되었습니다! 환영합니다.'); // router.replace(ROUTES.ACTIVITIES.ROOT); diff --git a/src/domain/Auth/hooks/useUser.ts b/src/domain/Auth/hooks/useUser.ts index 341039ca..c57c055d 100644 --- a/src/domain/Auth/hooks/useUser.ts +++ b/src/domain/Auth/hooks/useUser.ts @@ -1,12 +1,21 @@ import { useQuery } from '@tanstack/react-query'; +import { useEffect } from 'react'; import { getMe } from '@/domain/Auth/services'; +import { useRoamReadyStore } from '@/shared/store'; /** * @function useUser - * @description 현재 로그인된 사용자 정보를 가져오는 Tanstack Query 커스텀 훅입니다. + * @description + * 현재 로그인된 사용자의 정보를 가져와 클라이언트의 전역 상태(Zustand)와 동기화하는 커스텀 훅입니다. + * * 이 훅은 Tanstack Query의 `useQuery`를 사용하여 `getMe` 서비스 함수를 호출하고, - * 서버 상태(데이터, 로딩 상태, 에러 상태)를 자동으로 관리합니다. + * `useEffect`를 통해 쿼리의 결과(성공 또는 실패)에 따라 전역 상태를 업데이트합니다. + * + * - **조회 성공 시 (`isSuccess`)**: `setUser` 액션을 호출하여 전역 스토어에 최신 사용자 정보를 저장합니다. + * - **조회 실패 시 (`isError`)**: `clearUser` 액션을 호출하여 전역 스토어와 localStorage의 사용자 정보를 제거합니다. + * + * 이 방식은 앱이 시작될 때(`AuthInitializer` 내부에서 호출) 실제 서버의 인증 상태와 UI 상태의 불일치를 방지하는 핵심적인 역할을 수행합니다. * * @returns {object} Tanstack Query의 `useQueryResult` 객체. 포함하는 주요 속성은 다음과 같습니다: * - `data`: 성공 시 가져온 사용자 정보 (`User` 타입). @@ -17,20 +26,23 @@ import { getMe } from '@/domain/Auth/services'; * @property {Array} queryKey - Tanstack Query가 이 데이터를 식별하고 캐싱하는 고유한 키 (`['user', 'me']`). * @property {Function} queryFn - 실제 데이터를 가져오는 비동기 함수 (`getMe`). * @property {number} retry - 쿼리 실패 시 재시도 횟수 (1회). + * @property {number} staleTime - 데이터를 'fresh' 상태로 유지하는 시간 (5분). * * @example * ```typescript * import { useUser } from '@/domain/Auth/hooks/useUser'; * * function UserProfile() { - * const { data: user, isLoading, isError, error } = useUser(); + * const { data: user, isLoading, isError } = useUser(); * * if (isLoading) { * return 로딩 중...; * } * * if (isError) { - * return 에러: {error.message}; + * 이 컴포넌트가 렌더링될 시점에는 AuthInitializer가 이미 + * 로그아웃 처리를 완료했으므로, 보통 이 분기는 보이지 않습니다. + * return 로그인이 필요합니다.; * } * * return

안녕하세요, {user.nickname}님!

; @@ -38,9 +50,31 @@ import { getMe } from '@/domain/Auth/services'; * ``` */ export const useUser = () => { - return useQuery({ + const setUser = useRoamReadyStore((state) => state.setUser); + const clearUser = useRoamReadyStore((state) => state.clearUser); + + const queryResult = useQuery({ queryKey: ['user', 'me'], queryFn: getMe, retry: 1, + staleTime: 1000 * 60 * 5, }); + + // 2. useEffect를 사용하여 쿼리 결과에 따른 부가 작업을 처리합니다. + useEffect(() => { + // isSuccess가 true가 되면 (데이터 로딩 성공 시) + if (queryResult.isSuccess) { + setUser(queryResult.data); + } + }, [queryResult.isSuccess, queryResult.data, setUser]); + + useEffect(() => { + // isError가 true가 되면 (데이터 로딩 실패 시) + if (queryResult.isError) { + clearUser(); + } + }, [queryResult.isError, clearUser]); + + // 3. useQuery의 결과를 그대로 반환합니다. + return queryResult; }; diff --git a/src/domain/Auth/schemas/request.ts b/src/domain/Auth/schemas/request.ts index ea51abb3..8b83dedd 100644 --- a/src/domain/Auth/schemas/request.ts +++ b/src/domain/Auth/schemas/request.ts @@ -125,3 +125,22 @@ export const oauthSigninRequestSchema = z.object({ * @description OAuth 로그인 요청에 사용되는 타입입니다. */ export type OAuthSigninRequest = z.infer; + +/** + * @schema transitionNicknameSchema + * @description 카카오 전환 페이지에서 사용할 닉네임 입력값에 대한 유효성 검사 스키마입니다. + */ +export const transitionNicknameSchema = z.object({ + nickname: z + .string() + .min(1, { message: '닉네임을 입력해주세요.' }) + .max(10, { message: '닉네임은 10자 이하로 입력해주세요.' }), +}); + +/** + * @type TransitionNicknameFormValues + * @description 카카오 전환 페이지 폼의 데이터 타입입니다. + */ +export type TransitionNicknameFormValues = z.infer< + typeof transitionNicknameSchema +>; diff --git a/src/domain/Auth/utils/setAuthCookies.ts b/src/domain/Auth/utils/setAuthCookies.ts index 9350d2c6..9492b198 100644 --- a/src/domain/Auth/utils/setAuthCookies.ts +++ b/src/domain/Auth/utils/setAuthCookies.ts @@ -34,7 +34,8 @@ export default function setAuthCookies( secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', - maxAge: 60 * 60, + // maxAge: 60 * 60, //! 테스트를 위해 15초로 + maxAge: 15, }); response.cookies.set({ diff --git a/src/middleware.ts b/src/middleware.ts index 17a18418..087b6dd4 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -107,6 +107,7 @@ export async function middleware(request: NextRequest) { signal: AbortSignal.timeout(30000), }); + //! 여기서 분명히 갱신했는데 개발서버에서는 갱신이 되는데... 배포하면 안됨.. if (response.status === 401 && request.cookies.get('refreshToken')?.value) { const refreshToken = request.cookies.get('refreshToken')!.value; const refreshResponse = await fetch(
- 서연 + 서연 - 재현 + 재현
- 체험 등록 페이지
- 체험 수정 페이지
- 예약 현황 페이지
- 로그인 페이지
- 회원가입 페이지
- 404페이지
- 체험 상세 페이지 - 메인 페이지
- 마이페이지
- 메인페이지
- 마이페이지
공통 컴포넌트,
공통 로직
- Button
- SelectBox
- Tabs