Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
],
},
};
Expand Down
6 changes: 3 additions & 3 deletions src/apis/auth/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
};
10 changes: 5 additions & 5 deletions src/apis/users/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<User>('/users/me', updateUserForm);
const response = await axiosClientHelper.put<User>('/users/me', updateUserForm);
return response.data;
};

export const createProfileImage = async (createProfileImageForm: CreateProfileImageForm) => {
const response = await axiosHelper.post<CreateProfileImageSuccessResponse>('/users/me/image', createProfileImageForm, {
const response = await axiosClientHelper.post<CreateProfileImageSuccessResponse>('/users/me/image', createProfileImageForm, {
headers: {
'Content-Type': 'multipart/form-data',
},
Expand Down
2 changes: 1 addition & 1 deletion src/apis/users/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export type SignupFailResponse = FailResponse;

export type SignupResponse = Promise<SignupSuccessResponse | SignupFailResponse>;

export type GetUserResponse = Promise<User>;
export type GetUserResponse = Promise<{ user: User }>;

type ProfileImageUrl = string | URL | null;

Expand Down
10 changes: 0 additions & 10 deletions src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Header>오늘도 만나서 반가워요!</Header>
Expand Down
10 changes: 0 additions & 10 deletions src/app/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Header>첫 방문을 환영합니다!</Header>
Expand Down
92 changes: 92 additions & 0 deletions src/app/api/[...endpoint]/route.ts
Original file line number Diff line number Diff line change
@@ -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);
}
};
21 changes: 21 additions & 0 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -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*',
};
5 changes: 5 additions & 0 deletions src/utils/network/axiosClientHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import axios from 'axios';

const axiosClientHelper = axios.create({ baseURL: '/api' });

export default axiosClientHelper;
16 changes: 16 additions & 0 deletions src/utils/network/axiosServerHelper.ts
Original file line number Diff line number Diff line change
@@ -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;
19 changes: 19 additions & 0 deletions src/utils/network/errorResponse.ts
Original file line number Diff line number Diff line change
@@ -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;