diff --git a/next.config.ts b/next.config.ts
index 9dbb260..ced4173 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -3,13 +3,11 @@ import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
// Docker 배포를 위한 standalone 모드 활성화
// 해당 설정은 프로덕션 빌드 시 필요한 파일만 .next/standalone 폴더에 복사됨.
- images: {
- domains: ['sprint-fe-project.s3.ap-northeast-2.amazonaws.com'],
- },
output: 'standalone',
// 외부 이미지 도메인 허용
images: {
+ domains: ['sprint-fe-project.s3.ap-northeast-2.amazonaws.com'],
remotePatterns: [
{
protocol: 'https',
diff --git a/src/app/(non-header)/oauth/kakao/components/KakaoLoading.tsx b/src/app/(non-header)/oauth/kakao/components/KakaoLoading.tsx
new file mode 100644
index 0000000..5d1fe88
--- /dev/null
+++ b/src/app/(non-header)/oauth/kakao/components/KakaoLoading.tsx
@@ -0,0 +1,16 @@
+'use client';
+
+interface KakaoLoadingProps {
+ message: string;
+}
+
+export default function KakaoLoading({ message }: KakaoLoadingProps) {
+ return (
+
+ );
+}
diff --git a/src/app/(non-header)/oauth/kakao/sign-in/kakaoSigninCallbackPage.tsx b/src/app/(non-header)/oauth/kakao/sign-in/kakaoSigninCallbackPage.tsx
new file mode 100644
index 0000000..9ea5604
--- /dev/null
+++ b/src/app/(non-header)/oauth/kakao/sign-in/kakaoSigninCallbackPage.tsx
@@ -0,0 +1,117 @@
+'use client';
+
+export const dynamic = 'force-dynamic';
+
+import Popup from '@/components/Popup';
+import useUserStore from '@/stores/authStore';
+import { PopupState } from '@/types/popupTypes';
+import axios from 'axios';
+import { useRouter, useSearchParams } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import KakaoLoading from '../components/KakaoLoading';
+
+/**
+ * 카카오 로그인 콜백 처리 페이지 컴포넌트입니다.
+ *
+ * 카카오 인증 서버에서 리디렉션된 `code` 쿼리 파라미터를 받아
+ * 백엔드(`/api/auth/kakao/sign-in`)에 로그인 요청을 보냅니다.
+ * 응답에 포함된 사용자 정보를 Zustand 스토어에 저장하고,
+ * 로그인 성공 시 메인 페이지(`/`)로 이동합니다.
+ *
+ * 오류 발생 시 상태 코드에 따라 다른 알림 메시지를 출력하고
+ * 적절한 페이지(`/signup` 또는 `/login`)로 이동합니다.
+ *
+ * 주요 흐름:
+ * 1. `code` 파라미터 확인
+ * 2. POST 요청으로 로그인 시도
+ * 3. 사용자 존재 시 상태 저장 및 리다이렉트
+ * 4. 오류 상황에 따라 알림 및 경로 분기
+ *
+ */
+export default function KakaoSigninCallbackPage() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const setUser = useUserStore((state) => state.setUser);
+
+ const [popup, setPopup] = useState({
+ message: '',
+ redirect: '',
+ isOpen: false,
+ });
+
+ useEffect(() => {
+ const code = searchParams.get('code');
+ if (!code) return;
+
+ /**
+ * 카카오 로그인 처리를 위한 비동기 함수입니다.
+ * - 백엔드에 인증 코드 전송
+ * - 사용자 정보 저장
+ * - 오류 처리 및 알림
+ */
+ const handleKakaoLogin = async () => {
+ try {
+ const res = await axios.post('/api/auth/kakao/sign-in', { code });
+ const data = res.data;
+
+ if (data.user) {
+ setUser(data.user);
+ router.push('/');
+ }
+ } catch (err: unknown) {
+ if (axios.isAxiosError(err)) {
+ const status = err.response?.status;
+
+ switch (status) {
+ case 404:
+ setPopup({
+ message: '가입된 회원이 아닙니다. 회원가입을 진행해주세요.',
+ redirect: '/signup',
+ isOpen: true,
+ });
+ break;
+ case 500:
+ setPopup({
+ message: '서버 오류입니다. 잠시 후 다시 시도해주세요.',
+ redirect: '/login',
+ isOpen: true,
+ });
+ break;
+ default:
+ setPopup({
+ message: '카카오 로그인 실패',
+ redirect: '/login',
+ isOpen: true,
+ });
+ break;
+ }
+ } else {
+ setPopup({
+ message: '사용자 정보가 없습니다. 다시 시도해주세요.',
+ redirect: '/login',
+ isOpen: true,
+ });
+ }
+ }
+ };
+
+ handleKakaoLogin();
+ }, [searchParams, router]);
+
+ return (
+ <>
+
+
+ {
+ setPopup((prev) => ({ ...prev, isOpen: false }));
+ router.push(popup.redirect);
+ }}
+ >
+ {popup.message}
+
+ >
+ );
+}
diff --git a/src/app/(non-header)/oauth/kakao/sign-in/page.tsx b/src/app/(non-header)/oauth/kakao/sign-in/page.tsx
index dd1cdeb..8bbafde 100644
--- a/src/app/(non-header)/oauth/kakao/sign-in/page.tsx
+++ b/src/app/(non-header)/oauth/kakao/sign-in/page.tsx
@@ -1,86 +1,11 @@
-// 'use client';
-
-// export const dynamic = 'force-dynamic';
-
-// import useUserStore from '@/stores/authStore';
-// import axios from 'axios';
-// import { useRouter, useSearchParams } from 'next/navigation';
-// import { useEffect } from 'react';
-
-// /**
-// * 카카오 로그인 콜백 처리 페이지 컴포넌트입니다.
-// *
-// * 카카오 인증 서버에서 리디렉션된 `code` 쿼리 파라미터를 받아
-// * 백엔드(`/api/auth/kakao/sign-in`)에 로그인 요청을 보냅니다.
-// * 응답에 포함된 사용자 정보를 Zustand 스토어에 저장하고,
-// * 로그인 성공 시 메인 페이지(`/`)로 이동합니다.
-// *
-// * 오류 발생 시 상태 코드에 따라 다른 알림 메시지를 출력하고
-// * 적절한 페이지(`/signup` 또는 `/login`)로 이동합니다.
-// *
-// * 주요 흐름:
-// * 1. `code` 파라미터 확인
-// * 2. POST 요청으로 로그인 시도
-// * 3. 사용자 존재 시 상태 저장 및 리다이렉트
-// * 4. 오류 상황에 따라 알림 및 경로 분기
-// *
-// * @component
-// * @returns {JSX.Element} "카카오 로그인 처리 중입니다..."라는 텍스트를 포함한 JSX
-// */
-export default function KakaoSigninCallbackPage() {
- return 카카오 로그인 처리 예정
;
+import { Suspense } from 'react';
+import KakaoSigninCallbackPage from './kakaoSigninCallbackPage';
+import Loading from '@/components/Loading';
+
+export default function KakaoSigninPage() {
+ return (
+ }>
+
+
+ );
}
-// const router = useRouter();
-// const searchParams = useSearchParams();
-// const setUser = useUserStore((state) => state.setUser);
-
-// useEffect(() => {
-// const code = searchParams.get('code');
-// if (!code) return;
-
-// /**
-// * 카카오 로그인 처리를 위한 비동기 함수입니다.
-// * - 백엔드에 인증 코드 전송
-// * - 사용자 정보 저장
-// * - 오류 처리 및 알림
-// */
-// const handleKakaoLogin = async () => {
-// try {
-// const res = await axios.post('/api/auth/kakao/sign-in', { code });
-// const data = res.data;
-
-// if (data.user) {
-// setUser(data.user);
-// router.push('/');
-// }
-// } catch (err: unknown) {
-// if (axios.isAxiosError(err)) {
-// const status = err.response?.status;
-// const message = err.response?.data?.error;
-
-// switch (status) {
-// case 404:
-// alert('가입된 회원이 아닙니다. 회원가입을 진행해주세요.');
-// router.push('/signup');
-// break;
-// case 500:
-// alert('서버 오류입니다. 잠시 후 다시 시도해주세요.');
-// router.push('/login');
-// break;
-// default:
-// alert(message || '카카오 로그인 실패');
-// router.push('/login');
-// break;
-// }
-// } else {
-// alert('사용자 정보가 없습니다. 다시 시도해주세요.');
-// router.push('/login');
-// }
-// }
-// };
-
-// handleKakaoLogin();
-// }, [searchParams, router]);
-
-// return 카카오 로그인 처리 중입니다...
;
-// }
diff --git a/src/app/(non-header)/oauth/kakao/sign-up/kakaoSignupCallbackPage.tsx b/src/app/(non-header)/oauth/kakao/sign-up/kakaoSignupCallbackPage.tsx
new file mode 100644
index 0000000..f36721c
--- /dev/null
+++ b/src/app/(non-header)/oauth/kakao/sign-up/kakaoSignupCallbackPage.tsx
@@ -0,0 +1,143 @@
+'use client';
+
+export const dynamic = 'force-dynamic';
+
+import Popup from '@/components/Popup';
+import useUserStore from '@/stores/authStore';
+import { PopupState } from '@/types/popupTypes';
+import axios from 'axios';
+import { useRouter, useSearchParams } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import KakaoLoading from '../components/KakaoLoading';
+
+const adjectives = [
+ '상냥한',
+ '용감한',
+ '조용한',
+ '귀여운',
+ '멋진',
+ '차분한',
+ '빠른',
+ '신비한',
+];
+const animals = [
+ '고양이',
+ '호랑이',
+ '강아지',
+ '여우',
+ '곰',
+ '사자',
+ '토끼',
+ '다람쥐',
+];
+
+/**
+ * 카카오 회원가입 콜백 처리 페이지 컴포넌트입니다.
+ *
+ * 카카오 인증 서버에서 전달된 `code` 쿼리 파라미터를 바탕으로
+ * 백엔드(`/api/auth/kakao/sign-up`)에 회원가입 요청을 보내고,
+ * 성공적으로 가입된 사용자 정보를 Zustand 스토어에 저장한 후
+ * 로그인 페이지(`/login`)로 이동합니다.
+ *
+ * 요청 시 랜덤한 형용사 + 동물 조합으로 닉네임을 생성하여 함께 전송합니다.
+ *
+ * 주요 흐름:
+ * 1. `code` 쿼리 파라미터 존재 여부 확인
+ * 2. 랜덤 닉네임 생성
+ * 3. 서버에 회원가입 요청 전송
+ * 4. 성공 시 사용자 정보 저장 및 리다이렉션
+ * 5. 실패 시 알림 후 회원가입 페이지로 이동
+ */
+export default function KakaoSignupCallbackPage() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const setUser = useUserStore((state) => state.setUser);
+
+ const [popup, setPopup] = useState({
+ message: '',
+ redirect: '',
+ isOpen: false,
+ });
+
+ useEffect(() => {
+ const code = searchParams.get('code');
+
+ if (!code) return;
+
+ const nickname = `${adjectives[Math.floor(Math.random() * adjectives.length)]}${animals[Math.floor(Math.random() * animals.length)]}`;
+
+ /**
+ * 카카오 회원가입 처리를 위한 비동기 함수입니다.
+ * - 인증 코드와 랜덤 닉네임을 포함하여 서버에 POST 요청
+ * - 응답으로 받은 사용자 정보를 상태에 저장
+ * - 성공 시 로그인 페이지로, 실패 시 회원가입 페이지로 이동
+ */
+ const handleKakaoSignup = async () => {
+ try {
+ const res = await axios.post('/api/auth/kakao/sign-up', {
+ code,
+ nickname,
+ });
+ const data = res.data;
+
+ if (data.user) {
+ setUser(data.user);
+ router.push('/login');
+ }
+ } catch (err: unknown) {
+ if (axios.isAxiosError(err)) {
+ const status = err.response?.status;
+
+ switch (status) {
+ case 400:
+ setPopup({
+ message: '이미 가입된 회원입니다.',
+ redirect: '/login',
+ isOpen: true,
+ });
+ break;
+ case 500:
+ setPopup({
+ message: '서버 오류입니다. 잠시 후 다시 시도해주세요.',
+ redirect: '/signup',
+ isOpen: true,
+ });
+ break;
+ default:
+ setPopup({
+ message: '회원가입에 실패했습니다. 다시 시도해주세요.',
+ redirect: '/signup',
+ isOpen: true,
+ });
+ break;
+ }
+ } else {
+ setPopup({
+ message: '예기치 못한 오류가 발생했습니다. 다시 시도해주세요.',
+ redirect: '/signup',
+ isOpen: true,
+ });
+ }
+ }
+ };
+
+ handleKakaoSignup();
+ }, [searchParams, router]);
+
+ return (
+ <>
+
+
+ {
+ setPopup((prev) => ({ ...prev, isOpen: false }));
+ router.push(popup.redirect);
+ }}
+ >
+ {popup.message}
+
+ >
+ );
+}
diff --git a/src/app/(non-header)/oauth/kakao/sign-up/page.tsx b/src/app/(non-header)/oauth/kakao/sign-up/page.tsx
index 4234ccc..b64a505 100644
--- a/src/app/(non-header)/oauth/kakao/sign-up/page.tsx
+++ b/src/app/(non-header)/oauth/kakao/sign-up/page.tsx
@@ -1,93 +1,11 @@
-// 'use client';
-
-// export const dynamic = 'force-dynamic';
-
-// import useUserStore from '@/stores/authStore';
-// import axios from 'axios';
-// import { useRouter, useSearchParams } from 'next/navigation';
-// import { useEffect } from 'react';
-
-// const adjectives = [
-// '상냥한',
-// '용감한',
-// '조용한',
-// '귀여운',
-// '멋진',
-// '차분한',
-// '빠른',
-// '신비한',
-// ];
-// const animals = [
-// '고양이',
-// '호랑이',
-// '강아지',
-// '여우',
-// '곰',
-// '사자',
-// '토끼',
-// '다람쥐',
-// ];
-
-// /**
-// * 카카오 회원가입 콜백 처리 페이지 컴포넌트입니다.
-// *
-// * 카카오 인증 서버에서 전달된 `code` 쿼리 파라미터를 바탕으로
-// * 백엔드(`/api/auth/kakao/sign-up`)에 회원가입 요청을 보내고,
-// * 성공적으로 가입된 사용자 정보를 Zustand 스토어에 저장한 후
-// * 로그인 페이지(`/login`)로 이동합니다.
-// *
-// * 요청 시 랜덤한 형용사 + 동물 조합으로 닉네임을 생성하여 함께 전송합니다.
-// *
-// * 주요 흐름:
-// * 1. `code` 쿼리 파라미터 존재 여부 확인
-// * 2. 랜덤 닉네임 생성
-// * 3. 서버에 회원가입 요청 전송
-// * 4. 성공 시 사용자 정보 저장 및 리다이렉션
-// * 5. 실패 시 알림 후 회원가입 페이지로 이동
-// *
-// * @component
-// * @returns {JSX.Element} "카카오 회원가입 처리 중입니다..."라는 텍스트를 포함한 JSX
-// */
-export default function KakaoSignupCallbackPage() {
- return 회원가입 처리 예정
;
+import { Suspense } from 'react';
+import Loading from '@/components/Loading';
+import KakaoSignupCallbackPage from './kakaoSignupCallbackPage';
+
+export default function KakaoSignupPage() {
+ return (
+ }>
+
+
+ );
}
-// const router = useRouter();
-// const searchParams = useSearchParams();
-// const setUser = useUserStore((state) => state.setUser);
-
-// useEffect(() => {
-// const code = searchParams.get('code');
-
-// if (!code) return;
-
-// const nickname = `${adjectives[Math.floor(Math.random() * adjectives.length)]}${animals[Math.floor(Math.random() * animals.length)]}`;
-
-// /**
-// * 카카오 회원가입 처리를 위한 비동기 함수입니다.
-// * - 인증 코드와 랜덤 닉네임을 포함하여 서버에 POST 요청
-// * - 응답으로 받은 사용자 정보를 상태에 저장
-// * - 성공 시 로그인 페이지로, 실패 시 회원가입 페이지로 이동
-// */
-// const handleKakaoSignup = async () => {
-// try {
-// const res = await axios.post('/api/auth/kakao/sign-up', {
-// code,
-// nickname,
-// });
-// const data = res.data;
-
-// if (data.user) {
-// setUser(data.user);
-// router.push('/login');
-// }
-// } catch {
-// alert('카카오 회원가입 실패');
-// router.push('/signup');
-// }
-// };
-
-// handleKakaoSignup();
-// }, [searchParams, router]);
-
-// return 카카오 회원가입 처리 중입니다...
;
-// }
diff --git a/src/app/globals.css b/src/app/globals.css
index 55ccace..155833b 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -139,4 +139,4 @@
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
-}
\ No newline at end of file
+}
diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx
new file mode 100644
index 0000000..854913c
--- /dev/null
+++ b/src/components/Loading.tsx
@@ -0,0 +1,29 @@
+'use client';
+
+export default function Loading() {
+ return (
+
+ );
+}
diff --git a/src/middleware.ts b/src/middleware.ts
new file mode 100644
index 0000000..f34fe26
--- /dev/null
+++ b/src/middleware.ts
@@ -0,0 +1,47 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+/**
+ * Next.js Middleware 함수로, 인증 상태에 따라 사용자의 접근을 제어합니다.
+ *
+ * - 로그인/회원가입 페이지에 접근 시 이미 accessToken 또는 refreshToken이 존재하면 메인 페이지로 리디렉트
+ * - 보호된 페이지(`/mypage` 하위) 접근 시 토큰이 모두 없으면 로그인 페이지로 리디렉트
+ * - 그 외에는 요청을 그대로 통과시킴
+ *
+ * @param {NextRequest} request - 요청 객체로, 쿠키와 URL 경로 정보가 포함됨
+ * @returns {NextResponse} - 조건에 따른 리디렉트 응답 또는 요청 통과 응답
+ */
+export async function middleware(request: NextRequest) {
+ const accessToken = request.cookies.get('accessToken')?.value;
+ const refreshToken = request.cookies.get('refreshToken')?.value;
+
+ const { pathname } = request.nextUrl;
+
+ // 로그인/회원가입 페이지 접근 시 이미 로그인 상태면 메인으로 리디렉트
+ if (
+ (pathname === '/login' || pathname === '/signup') &&
+ (accessToken || refreshToken)
+ ) {
+ return NextResponse.redirect(new URL('/', request.url));
+ }
+
+ // 보호 경로 설정 및 검사
+ const protectedPaths = ['/mypage'];
+ const isProtected = protectedPaths.some((path) => pathname.startsWith(path));
+
+ // 보호 경로 접근 시 두 토큰 모두 없으면 로그인으로 리디렉트
+ if (isProtected && !accessToken && !refreshToken) {
+ return NextResponse.redirect(new URL('/login', request.url));
+ }
+
+ // 조건에 해당하지 않으면 다음 응답으로 진행
+ return NextResponse.next();
+}
+
+/**
+ * 미들웨어가 적용될 경로를 지정하는 설정 객체입니다.
+ * - '/login', '/signup' 경로
+ * - '/mypage'와 그 하위 경로들 ('/mypage/profile', '/mypage/dashboard' 등)
+ */
+export const config = {
+ matcher: ['/login', '/signup', '/mypage/:path*'],
+};
diff --git a/src/types/popupTypes.ts b/src/types/popupTypes.ts
index 8baeecf..f6bebee 100644
--- a/src/types/popupTypes.ts
+++ b/src/types/popupTypes.ts
@@ -23,3 +23,9 @@ export interface PopupProps {
onClose: () => void;
onConfirm?: () => void;
}
+
+export interface PopupState {
+ message: string;
+ redirect: string;
+ isOpen: boolean;
+}