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)
+
+#### 회원가입/로그인
+
+
+
+
+
+#### OAuth(kakao)
+
+
+
+---
+
+#### 메인페이지 캐러셀
+
+
+
+#### 메인페이지 리스트
+
+
+
+
+#### 메인페이지 검색
+
+
+
+---
+
+#### 마이페이지 내 정보 수정
+
+
+
+#### 마이페이지 예약 관리
+
+
+
+#### 마이페이지 내 체험 관리
+
+
+
+#### 마이페이지 내 예약 현황
+
+
+
+---
+
+#### 상세페이지
+
+
+
+---
+
+#### 체험 등록
+
+
+
+
+
+
---
@@ -29,10 +88,10 @@
-
+
|
-
+
|
@@ -47,7 +106,7 @@
- 체험 등록 페이지 - 체험 수정 페이지 - 예약 현황 페이지 |
- 로그인 페이지 - 회원가입 페이지 - 404페이지 |
- 체험 상세 페이지 |
- - 메인 페이지 - 마이페이지 |
+ - 메인페이지 - 마이페이지 |
공통 컴포넌트, 공통 로직 |
- Button - SelectBox - Tabs |
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 ?? '아래 버튼을 눌러 회원가입하세요.'}
-
-