diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 38a0c33a..83c90371 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -77,3 +77,5 @@ jobs:
- name: Build test
run: npm run build
+ env:
+ NEXT_PUBLIC_API_BASE_URL: ${{secrets.NEXT_PUBLIC_API_BASE_URL}}
diff --git a/next.config.mjs b/next.config.mjs
index 652ed24d..8cb14913 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -30,5 +30,13 @@ const nextConfig = {
},
];
},
+ async rewrites() {
+ return [
+ {
+ source: '/:path*',
+ destination: `${process.env.NEXT_PUBLIC_API_BASE_URL}/:path*`,
+ },
+ ];
+ },
};
export default nextConfig;
diff --git a/src/_apis/auth/auth-apis.tsx b/src/_apis/auth/auth-apis.tsx
index bfa6a7fb..51a931ca 100644
--- a/src/_apis/auth/auth-apis.tsx
+++ b/src/_apis/auth/auth-apis.tsx
@@ -2,27 +2,33 @@ import { fetchApi } from '@/src/utils/api';
import { LoginRequest, LoginResponse, SignupRequest, SignupResponse, User } from '@/src/types/auth';
export function signupUser(data: SignupRequest): Promise<{ data: SignupResponse }> {
- return fetchApi<{ data: SignupResponse }>('/signup', {
+ return fetchApi<{ data: SignupResponse; headers: Headers }>('/auths/signup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
+ }).then((response) => {
+ const token = response.headers.get('Authorization');
+ return { data: { token } };
});
}
export function loginUser(data: LoginRequest): Promise<{ data: LoginResponse }> {
- return fetchApi<{ data: LoginResponse }>('/login', {
+ return fetchApi<{ data: LoginResponse; headers: Headers }>('/auths/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
+ }).then((response) => {
+ const token = response.headers.get('Authorization');
+ return { data: { token } };
});
}
export function getUser(): Promise<{ data: User }> {
- return fetchApi<{ data: User }>('/user/1', {
+ return fetchApi<{ data: User }>('/auths/user', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
diff --git a/src/_queries/auth/auth-queries.tsx b/src/_queries/auth/auth-queries.tsx
index ef7065f1..abd8418a 100644
--- a/src/_queries/auth/auth-queries.tsx
+++ b/src/_queries/auth/auth-queries.tsx
@@ -1,36 +1,28 @@
-import { ApiError } from 'next/dist/server/api-utils';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { getUser, loginUser, signupUser } from '@/src/_apis/auth/auth-apis';
import { useAuthStore } from '@/src/store/use-auth-store';
+import { ApiError } from '@/src/utils/api';
import { transformKeysToCamel } from '@/src/utils/transform-keys';
import { LoginRequest, LoginResponse, SignupRequest, SignupResponse, User } from '@/src/types/auth';
export function usePostSignupQuery() {
- const queryClient = useQueryClient();
- const { login } = useAuthStore();
+ const handleAuthSuccess = useHandleAuthSuccess();
return useMutation<{ data: SignupResponse }, ApiError, SignupRequest>({
mutationFn: signupUser,
onSuccess: async (response) => {
- // TODO: token값으로 수정, const { token } = response.data;
- const token = 'dummyToken123';
- const user: User = await queryClient.fetchQuery(getUserQuery());
- login(user, token);
+ await handleAuthSuccess(response.data.token);
},
});
}
export function usePostLoginQuery() {
- const queryClient = useQueryClient();
- const { login } = useAuthStore();
+ const handleAuthSuccess = useHandleAuthSuccess();
return useMutation<{ data: LoginResponse }, ApiError, LoginRequest>({
mutationFn: loginUser,
onSuccess: async (response) => {
- // TODO: token값으로 수정, const { token } = response.data;
- const token = 'dummyToken123';
- const user: User = await queryClient.fetchQuery(getUserQuery());
- login(user, token);
+ await handleAuthSuccess(response.data.token);
},
});
}
@@ -42,3 +34,23 @@ export function getUserQuery() {
select: (data: User) => transformKeysToCamel(data),
};
}
+
+function useHandleAuthSuccess() {
+ const queryClient = useQueryClient();
+ const { login, setUser } = useAuthStore();
+
+ return async function handleAuthSuccess(token: string | null) {
+ if (!token) {
+ throw new Error('토큰이 없습니다');
+ }
+
+ try {
+ const accessToken = token.replace(/^Bearer\s/, '');
+ login(accessToken);
+ const user: User = await queryClient.fetchQuery(getUserQuery());
+ setUser(user);
+ } catch (error) {
+ throw new Error('사용자 상태 업데이트 실패');
+ }
+ };
+}
diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx
index f880472a..3cef2e25 100644
--- a/src/app/(auth)/layout.tsx
+++ b/src/app/(auth)/layout.tsx
@@ -19,10 +19,10 @@ export default function AuthLayout({
-
+
-
+
Welcome,
크루에 오신 것을 환영합니다 🙌
@@ -30,7 +30,7 @@ export default function AuthLayout({
함께할 사람이없나요? 지금 크루에 참여해보세요
-
{children}
+
{children}
diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx
index 4f5f9ea3..7181f694 100644
--- a/src/app/(auth)/login/page.tsx
+++ b/src/app/(auth)/login/page.tsx
@@ -19,15 +19,14 @@ export default function LoginPage() {
router.push('/');
},
onError: (error) => {
- if (error.statusCode === 404) {
+ if (error.status === 401) {
setError('email', {
type: 'manual',
- message: '이메일 계정이 존재하지 않습니다',
+ message: '이메일 또는 비밀번호가 일치하지 않습니다.',
});
- } else if (error.statusCode === 401) {
setError('password', {
type: 'manual',
- message: '잘못된 비밀번호입니다',
+ message: '이메일 또는 비밀번호가 일치하지 않습니다.',
});
}
},
diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx
index b7e0f0e3..40f58225 100644
--- a/src/app/(auth)/signup/page.tsx
+++ b/src/app/(auth)/signup/page.tsx
@@ -14,18 +14,21 @@ export default function LoginPage() {
const { mutate: postSignup } = usePostSignupQuery();
const handleSubmit = async (data: SignupFormValues) => {
- postSignup(data, {
+ const { confirmPassword, ...requestData } = data;
+
+ postSignup(requestData, {
onSuccess: () => {
router.push('/');
},
onError: (error) => {
- if (error.statusCode === 400) {
- // TODO: parameter 처리 후 message 처리 확인
- // const { parameter } = error.parameter;
- // setError(parameter, {
- // type: 'manual',
- // message: error.message,
- // });
+ if (error.status === 400) {
+ const { validationErrors } = error.detail;
+ Object.keys(validationErrors).forEach((key) => {
+ setError(key as 'email', {
+ type: 'manual',
+ message: validationErrors[key],
+ });
+ });
}
},
});
diff --git a/src/components/common/header/header.stories.tsx b/src/components/common/header/header.stories.tsx
index 2dda18fd..b72ce618 100644
--- a/src/components/common/header/header.stories.tsx
+++ b/src/components/common/header/header.stories.tsx
@@ -21,7 +21,7 @@ const meta: Meta = {
export default meta;
function Template() {
- const { isAuth, login, logout } = useAuthStore();
+ const { isAuth, login, logout, setUser } = useAuthStore();
const testToken = 'test token';
const testUser = {
id: 1,
@@ -34,7 +34,8 @@ function Template() {
if (isAuth) {
logout();
} else {
- login(testUser, testToken);
+ login(testToken);
+ setUser(testUser);
}
};
diff --git a/src/store/use-auth-store.tsx b/src/store/use-auth-store.tsx
index 7a0e373f..afc68bd3 100644
--- a/src/store/use-auth-store.tsx
+++ b/src/store/use-auth-store.tsx
@@ -6,8 +6,9 @@ interface AuthState {
isAuth: boolean;
user: User | null;
token: string | null;
- login: (userData: User, token: string) => void;
+ login: (token: string) => void;
logout: () => void;
+ setUser: (user: User) => void;
}
export const useAuthStore = create
()(
@@ -16,10 +17,9 @@ export const useAuthStore = create()(
isAuth: false,
user: null,
token: null,
- login: (userData, token) =>
+ login: (token) =>
set({
isAuth: true,
- user: userData,
token,
}),
logout: () =>
@@ -28,6 +28,7 @@ export const useAuthStore = create()(
user: null,
token: null,
}),
+ setUser: (user: User) => set((state) => ({ ...state, user })),
}),
{
name: 'auth-storage',
diff --git a/src/types/auth.ts b/src/types/auth.ts
index 9f67ff08..d6da4ea9 100644
--- a/src/types/auth.ts
+++ b/src/types/auth.ts
@@ -1,5 +1,5 @@
export interface SignupResponse {
- token: string;
+ token: string | null;
}
export interface SignupRequest {
@@ -9,7 +9,7 @@ export interface SignupRequest {
}
export interface LoginResponse {
- token: string;
+ token: string | null;
}
export interface LoginRequest {
diff --git a/src/utils/api.ts b/src/utils/api.ts
index adfc7154..4ca84740 100644
--- a/src/utils/api.ts
+++ b/src/utils/api.ts
@@ -1,15 +1,16 @@
import { useAuthStore } from '@/src/store/use-auth-store';
-// TODO: 추후 API URL 수정
-const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3009';
-
export class ApiError extends Error {
+ detail: { validationErrors: Record } = { validationErrors: {} };
+
constructor(
public status: number,
message: string,
+ detail?: { validationErrors: Record }, // 타입을 맞춤
) {
super(message);
this.name = 'ApiError';
+ if (detail) this.detail = detail;
}
}
@@ -23,7 +24,6 @@ export async function fetchApi(
const { signal } = controller;
const id = setTimeout(() => controller.abort(), timeout);
const { token } = useAuthStore.getState();
-
const fetchOptions: RequestInit = {
...options,
signal,
@@ -35,21 +35,23 @@ export async function fetchApi(
};
try {
- const response = await fetch(`${API_BASE_URL}${url}`, fetchOptions); // API 요청 실행
+ const response = await fetch(`${url}`, fetchOptions); // API 요청 실행
if (!response.ok) {
+ let errorDetail;
let errorMessage;
try {
- const errorData = await response.json();
- errorMessage = errorData.message || `HTTP error! status: ${response.status}`;
+ const { status, message, ...detail } = await response.json();
+ errorMessage = message || `HTTP error! status: ${response.status}`;
+ errorDetail = detail;
} catch {
errorMessage = `HTTP error! status: ${response.status}`;
}
- throw new ApiError(response.status, errorMessage);
+ throw new ApiError(response.status, errorMessage, errorDetail);
}
- // 응답 데이터를 JSON 형태로 반환
- return (await response.json()) as T;
+ const data = await response.json();
+ return { ...data, headers: response.headers } as T;
} catch (error) {
if (error instanceof Error) {
if (error.name === 'AbortError') throw new ApiError(408, 'Request timeout');