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 ( +
+
+
+

{message}

+
+
+ ); +} 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; +}