diff --git a/next.config.ts b/next.config.ts index 9d5b0f1..5bc8cca 100644 --- a/next.config.ts +++ b/next.config.ts @@ -14,6 +14,10 @@ const nextConfig: NextConfig = { protocol: 'https', hostname: 'images.unsplash.com', }, + { + protocol: 'https', + hostname: 'sprint-fe-project.s3.ap-northeast-2.amazonaws.com', + }, ], }, }; diff --git a/src/apis/auth/index.ts b/src/apis/auth/index.ts index ad37408..2ea9e25 100644 --- a/src/apis/auth/index.ts +++ b/src/apis/auth/index.ts @@ -1,11 +1,11 @@ -import axiosHelper from '@/utils/network/axiosHelper'; +import axiosClientHelper from '@/utils/network/axiosClientHelper'; import { LoginFormData, LoginResponse, PutPasswordFormData, PutPasswordResponse } from './types'; import { isAxiosError } from 'axios'; import { isError } from 'es-toolkit/compat'; export const login = async (loginFormData: LoginFormData): LoginResponse => { try { - const response = await axiosHelper.post('/auth/login', loginFormData); + const response = await axiosClientHelper.post('/auth/login', loginFormData); return response.data; } catch (error) { if (isAxiosError(error)) return error.response?.data; @@ -14,5 +14,5 @@ export const login = async (loginFormData: LoginFormData): LoginResponse => { }; export const putPassword = async (putPasswordFormData: PutPasswordFormData): PutPasswordResponse => { - await axiosHelper.put('/auth/password', putPasswordFormData); + await axiosClientHelper.put('/auth/password', putPasswordFormData); }; diff --git a/src/apis/users/index.ts b/src/apis/users/index.ts index 5ba5bfd..2bdf764 100644 --- a/src/apis/users/index.ts +++ b/src/apis/users/index.ts @@ -1,11 +1,11 @@ -import axiosHelper from '@/utils/network/axiosHelper'; +import axiosClientHelper from '@/utils/network/axiosClientHelper'; import { CreateProfileImageForm, CreateProfileImageSuccessResponse, GetUserResponse, SignupFormData, SignupResponse, UpdateUserForm, User } from './types'; import { isAxiosError } from 'axios'; import { isError } from 'es-toolkit/compat'; export const signup = async (signupFormData: SignupFormData): SignupResponse => { try { - const response = await axiosHelper.post('/users', signupFormData); + const response = await axiosClientHelper.post('/users', signupFormData); return response.data; } catch (error) { if (isAxiosError(error)) return error.response?.data; @@ -16,17 +16,17 @@ export const signup = async (signupFormData: SignupFormData): SignupResponse => }; export const getUser = async (): GetUserResponse => { - const response = await axiosHelper.get('/users/me'); + const response = await axiosClientHelper.get('/users/me'); return response.data; }; export const updateUser = async (updateUserForm: UpdateUserForm) => { - const response = await axiosHelper.put('/users/me', updateUserForm); + const response = await axiosClientHelper.put('/users/me', updateUserForm); return response.data; }; export const createProfileImage = async (createProfileImageForm: CreateProfileImageForm) => { - const response = await axiosHelper.post('/users/me/image', createProfileImageForm, { + const response = await axiosClientHelper.post('/users/me/image', createProfileImageForm, { headers: { 'Content-Type': 'multipart/form-data', }, diff --git a/src/apis/users/types.ts b/src/apis/users/types.ts index 15ec8c6..6548065 100644 --- a/src/apis/users/types.ts +++ b/src/apis/users/types.ts @@ -39,7 +39,7 @@ export type SignupFailResponse = FailResponse; export type SignupResponse = Promise; -export type GetUserResponse = Promise; +export type GetUserResponse = Promise<{ user: User }>; type ProfileImageUrl = string | URL | null; diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index 2551c53..f7b9516 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -1,20 +1,10 @@ 'use client'; -import useAuthStore from '@/stores/authStore'; -import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; import Link from 'next/link'; import LoginForm from '@/components/auth/LoginForm'; import Header from '@/components/auth/Header'; export default function Login() { - const router = useRouter(); - const { accessToken } = useAuthStore(); - - useEffect(() => { - if (accessToken) router.push('/mydashboard'); - }, [router, accessToken]); - return ( <>
오늘도 만나서 반가워요!
diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx index 8587a3a..9008973 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/(auth)/signup/page.tsx @@ -1,20 +1,10 @@ 'use client'; -import useAuthStore from '@/stores/authStore'; -import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; import Link from 'next/link'; import SignupForm from '@/components/auth/SignupForm'; import Header from '@/components/auth/Header'; export default function Signup() { - const router = useRouter(); - const { accessToken } = useAuthStore(); - - useEffect(() => { - if (accessToken) router.push('/mydashboard'); - }, [router, accessToken]); - return ( <>
첫 방문을 환영합니다!
diff --git a/src/app/api/[...endpoint]/route.ts b/src/app/api/[...endpoint]/route.ts new file mode 100644 index 0000000..e615e57 --- /dev/null +++ b/src/app/api/[...endpoint]/route.ts @@ -0,0 +1,92 @@ +/** + * + * proxy처럼 사용하기 위해 작성한 route입니다. + * client에서 배포사이트_도메인/api/*으로 지정한 request는 해당 파일을 거칩니다. + * + * api 요청 흐름은 다음과 같습니다. + * 1. client에서 axiosClient.HTTP메소드(); 로 next 서버에 api 요청을 보냅니다. + * 2. next 서버에서 각 HTTP 메소드에 맞는 메소드를 실행합니다. + * 3. server에서 axiosServer.HTTP메소드();로 외부 백엔드 서버에 api 요청을 보냅니다. + * 4. axios 인터셉터에서 cookie를 읽고, Authorization Bearer 토큰을 설정해서 요청을 보냅니다. + * 5. 외부 백엔드 서버로부터 받은 응답을 client에게 응답합니다. + * + */ + +import axiosServerHelper from '@/utils/network/axiosServerHelper'; +import errorResponse from '@/utils/network/errorResponse'; +import { isEmpty, omit } from 'es-toolkit/compat'; +import { NextRequest, NextResponse } from 'next/server'; + +export const GET = async (request: NextRequest) => { + const url = new URL(request.url); + const endPoint = url.pathname.replace(/^\/api/, ''); + const searchParams = Object.fromEntries(url.searchParams.entries()); + try { + const apiResponse = await axiosServerHelper.get( + endPoint, + !isEmpty(searchParams) + ? { + params: searchParams, + } + : {}, + ); + return NextResponse.json(apiResponse.data, { status: apiResponse.status }); + } catch (error) { + return errorResponse(error); + } +}; + +export const POST = async (request: NextRequest) => { + const url = new URL(request.url); + const endPoint = url.pathname.replace(/^\/api/, ''); + const contentType = request.headers.get('Content-Type')?.split(';')[0]; + + try { + const apiResponse = await axiosServerHelper.post(endPoint, contentType === 'application/json' ? await request.json() : await request.formData(), { + headers: { + 'Content-Type': request.headers.get('Content-Type'), + }, + }); + const response = NextResponse.json(omit(apiResponse.data, ['accessToken']), { status: apiResponse.status }); + if (endPoint === '/auth/login') + response.cookies.set('accessToken', apiResponse.data.accessToken, { + httpOnly: true, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + path: '/', + }); + return response; + } catch (error) { + return errorResponse(error); + } +}; + +export const PUT = async (request: NextRequest) => { + const url = new URL(request.url); + const endPoint = url.pathname.replace(/^\/api/, ''); + try { + const apiResponse = await axiosServerHelper.put(endPoint, await request.json()); + if (isEmpty(apiResponse.data)) + return new NextResponse(null, { + status: apiResponse.status, + }); + return NextResponse.json(apiResponse.data, { status: apiResponse.status }); + } catch (error) { + return errorResponse(error); + } +}; + +export const DELETE = async (request: NextRequest) => { + const url = new URL(request.url); + const endPoint = url.pathname.replace(/^\/api/, ''); + try { + const apiResponse = await axiosServerHelper.delete(endPoint); + if (isEmpty(apiResponse.data)) + return new NextResponse(null, { + status: apiResponse.status, + }); + return NextResponse.json(apiResponse.data, { status: apiResponse.status }); + } catch (error) { + return errorResponse(error); + } +}; diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..6a10901 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,21 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; + +const AFTER_LOGIN_DOMAIN = ['/mydashboard', '/dashboard/:path*', '/dashboard', '/mypage'] satisfies readonly string[]; +const BEFORE_LOGIN_DOMAIN = ['/faq', '/privacy', '/login', '/signup', '/'] satisfies readonly string[]; + +export const middleware = async (request: NextRequest) => { + const cookieStore = await cookies(); + const accessToken = cookieStore.get('accessToken'); + if (!accessToken?.value) { + if (AFTER_LOGIN_DOMAIN.includes(request.nextUrl.pathname)) return NextResponse.redirect(new URL('/login', request.url)); + } else { + if (BEFORE_LOGIN_DOMAIN.includes(request.nextUrl.pathname)) return NextResponse.redirect(new URL('/mydashboard', request.url)); + } + + return NextResponse.next(); +}; + +export const config = { + matcher: '/:path*', +}; diff --git a/src/utils/network/axiosClientHelper.ts b/src/utils/network/axiosClientHelper.ts new file mode 100644 index 0000000..bf16830 --- /dev/null +++ b/src/utils/network/axiosClientHelper.ts @@ -0,0 +1,5 @@ +import axios from 'axios'; + +const axiosClientHelper = axios.create({ baseURL: '/api' }); + +export default axiosClientHelper; diff --git a/src/utils/network/axiosServerHelper.ts b/src/utils/network/axiosServerHelper.ts new file mode 100644 index 0000000..f957b96 --- /dev/null +++ b/src/utils/network/axiosServerHelper.ts @@ -0,0 +1,16 @@ +import axios from 'axios'; +import { cookies } from 'next/headers'; + +const axiosServerHelper = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL, +}); + +axiosServerHelper.interceptors.request.use(async (config) => { + const cookieStore = await cookies(); + const accessToken = cookieStore.get('accessToken'); + + if (accessToken?.value) config.headers.Authorization = `Bearer ${accessToken.value}`; + + return config; +}); +export default axiosServerHelper; diff --git a/src/utils/network/errorResponse.ts b/src/utils/network/errorResponse.ts new file mode 100644 index 0000000..caced41 --- /dev/null +++ b/src/utils/network/errorResponse.ts @@ -0,0 +1,19 @@ +import { isAxiosError } from 'axios'; +import { isError } from 'es-toolkit/compat'; +import { NextResponse } from 'next/server'; + +const errorResponse = (error: unknown) => { + if (isAxiosError(error)) { + return NextResponse.json(error.response?.data, { + status: error.response?.status ?? 500, + }); + } + return NextResponse.json( + { + message: isError(error) ? error.message : String(error), + }, + { status: 500 }, + ); +}; + +export default errorResponse;