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({
-
+
auth
-
+
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');