diff --git a/src/api/index.ts b/src/api/index.ts index 91447655..ef0330a5 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,10 +1,12 @@ -import { userServiceRemote } from './service'; +import { authServiceRemote, userServiceRemote } from './service'; const provideAPIService = () => { const userService = userServiceRemote(); + const authService = authServiceRemote(); return { usersService: userService, + authService, }; }; diff --git a/src/api/service/auth-service/index.ts b/src/api/service/auth-service/index.ts new file mode 100644 index 00000000..63ae05e6 --- /dev/null +++ b/src/api/service/auth-service/index.ts @@ -0,0 +1,33 @@ +import type { AxiosError } from 'axios'; + +import { baseAPI } from '@/api/core'; +import { LoginRequest, LoginResponse, SignupRequest, SignupResponse } from '@/types/service/auth'; +import { CommonErrorResponse } from '@/types/service/common'; + +export const isProblemDetailError = (error: unknown): error is AxiosError => { + return ( + typeof error === 'object' && + error !== null && + 'isAxiosError' in error && + (error as AxiosError).isAxiosError === true + ); +}; + +export const authServiceRemote = () => ({ + // 로그인 + login: async (payload: LoginRequest): Promise => { + const { data } = await baseAPI.post('/api/v1/auth/login', payload); + return data; + }, + + // 회원가입 + signup: async (payload: SignupRequest): Promise => { + const { data } = await baseAPI.post('/api/v1/auth/signup', payload); + return data; + }, + + // 로그아웃 + logout: async (): Promise => { + await baseAPI.post('/api/v1/auth/logout'); + }, +}); diff --git a/src/api/service/index.ts b/src/api/service/index.ts index a72b81ea..bf5b1ae7 100644 --- a/src/api/service/index.ts +++ b/src/api/service/index.ts @@ -1 +1,2 @@ +export * from './auth-service'; export * from './user-service'; diff --git a/src/components/pages/login/login-form/index.tsx b/src/components/pages/login/login-form/index.tsx index 22689bdd..c0e3b142 100644 --- a/src/components/pages/login/login-form/index.tsx +++ b/src/components/pages/login/login-form/index.tsx @@ -1,7 +1,11 @@ 'use client'; +import { useRouter } from 'next/navigation'; + import { type AnyFieldApi, useForm } from '@tanstack/react-form'; +import { API } from '@/api'; +import { isProblemDetailError } from '@/api/service'; import { FormInput } from '@/components/shared'; import { Button } from '@/components/ui'; import { loginSchema } from '@/lib/schema/auth'; @@ -19,6 +23,8 @@ const getHintMessage = (field: AnyFieldApi) => { }; export const LoginForm = () => { + const router = useRouter(); + const form = useForm({ defaultValues: { email: '', @@ -28,9 +34,29 @@ export const LoginForm = () => { onSubmit: loginSchema, onChange: loginSchema, }, - onSubmit: async ({ value }) => { - // API 호출 - alert('login:' + value.email); + onSubmit: async ({ value, formApi }) => { + try { + const payload = { + email: value.email, + password: value.password, + }; + + const result = await API.authService.login(payload); + console.log('login success:', result); + + formApi.reset(); + router.push('/'); + } catch (error) { + if (isProblemDetailError(error) && error.response?.data) { + const problem = error.response.data; + + console.error('[LOGIN ERROR]', problem.errorCode, problem.detail); + alert(problem.detail || '로그인에 실패했습니다.'); + } else { + console.error(error); + alert('알 수 없는 오류가 발생했습니다.'); + } + } }, }); diff --git a/src/components/pages/signup/signup-form/index.tsx b/src/components/pages/signup/signup-form/index.tsx index 81d02e48..efede055 100644 --- a/src/components/pages/signup/signup-form/index.tsx +++ b/src/components/pages/signup/signup-form/index.tsx @@ -1,7 +1,11 @@ 'use client'; +import { useRouter } from 'next/navigation'; + import { type AnyFieldApi, useForm } from '@tanstack/react-form'; +import { API } from '@/api'; +import { isProblemDetailError } from '@/api/service'; import { FormInput } from '@/components/shared'; import { Button } from '@/components/ui'; import { signupSchema } from '@/lib/schema/auth'; @@ -19,6 +23,8 @@ const getHintMessage = (field: AnyFieldApi) => { }; export const SignupForm = () => { + const router = useRouter(); + const form = useForm({ defaultValues: { email: '', @@ -30,9 +36,30 @@ export const SignupForm = () => { onChange: signupSchema, onSubmit: signupSchema, }, - onSubmit: async ({ value }) => { - // api 호출 - alert('signup:' + value.nickname); + onSubmit: async ({ value, formApi }) => { + try { + const payload = { + email: value.email, + password: value.password, + nickName: value.nickname, + }; + + const result = await API.authService.signup(payload); + console.log('signup success:', result); + + formApi.reset(); + router.push('/login'); + } catch (error) { + if (isProblemDetailError(error) && error.response?.data) { + const problem = error.response.data; + + console.error('[SIGNUP ERROR]', problem.errorCode, problem.detail); + alert(problem.detail || '회원가입에 실패했습니다.'); + } else { + console.error(error); + alert('알 수 없는 오류가 발생했습니다.'); + } + } }, }); diff --git a/src/mock/handlers.ts b/src/mock/handlers.ts index 458d6f33..31528087 100644 --- a/src/mock/handlers.ts +++ b/src/mock/handlers.ts @@ -1,3 +1,4 @@ +import { authHandlers } from './service/auth/auth-handlers'; import { userHandlers } from './service/user/users-handler'; -export const handlers = [...userHandlers]; +export const handlers = [...userHandlers, ...authHandlers]; diff --git a/src/mock/service/auth/auth-handlers.ts b/src/mock/service/auth/auth-handlers.ts new file mode 100644 index 00000000..b940e398 --- /dev/null +++ b/src/mock/service/auth/auth-handlers.ts @@ -0,0 +1,66 @@ +import { http, HttpResponse } from 'msw'; + +import { LoginRequest, LoginResponse, SignupRequest } from '@/types/service/auth'; +import { CommonErrorResponse } from '@/types/service/common'; + +import { + createLoginResponse, + createSignupResponse, + isEmailTaken, + isNicknameTaken, +} from './auth-utils'; + +const signupMock = http.post('*/api/v1/auth/signup', async ({ request }) => { + const body = (await request.json()) as SignupRequest; + + if (isEmailTaken(body.email)) { + const errorBody: CommonErrorResponse = { + type: 'https://example.com/errors/email-duplicate', + title: 'EMAIL_DUPLICATE', + status: 400, + detail: '이미 존재하는 이메일입니다.', + instance: '/api/v1/auth/signup', + errorCode: 'A002', + }; + return HttpResponse.json(errorBody, { status: 400 }); + } + + if (isNicknameTaken(body.nickName)) { + const errorBody: CommonErrorResponse = { + type: 'https://example.com/errors/nickname-duplicate', + title: 'NICKNAME_DUPLICATE', + status: 400, + detail: '이미 존재하는 닉네임입니다.', + instance: '/api/v1/auth/signup', + errorCode: 'A003', + }; + return HttpResponse.json(errorBody, { status: 400 }); + } + const response = createSignupResponse(body.email, body.nickName, body.password); + return HttpResponse.json(response, { status: 201 }); +}); + +const loginMock = http.post('*/api/v1/auth/login', async ({ request }) => { + const body = (await request.json()) as LoginRequest; + + try { + const response = createLoginResponse(body.email, body.password); + return HttpResponse.json(response, { status: 200 }); + } catch { + const errorBody: CommonErrorResponse = { + type: 'https://example.com/errors/invalid-credentials', + title: 'INVALID_CREDENTIALS', + status: 400, + detail: '이메일 또는 비밀번호가 올바르지 않습니다.', + instance: '/api/v1/auth/login', + errorCode: 'A001', + }; + return HttpResponse.json(errorBody, { status: 400 }); + } +}); + +const logoutMock = http.post('*/api/v1/auth/logout', async ({}) => { + return new HttpResponse(null, { status: 204 }); +}); + +export const authHandlers = [signupMock, loginMock, logoutMock]; diff --git a/src/mock/service/auth/auth-mock.ts b/src/mock/service/auth/auth-mock.ts new file mode 100644 index 00000000..0a6ff231 --- /dev/null +++ b/src/mock/service/auth/auth-mock.ts @@ -0,0 +1,31 @@ +export interface AuthMockUser { + id: number; + email: string; + nickName: string; + password: string; + createdAt: string; +} + +export const authMockUsers: AuthMockUser[] = [ + { + id: 1, + email: 'messi@test.com', + nickName: '리오넬 메시', + password: 'test9876', + createdAt: '2025-01-30T12:00:00', + }, + { + id: 2, + email: 'ronaldo@test.com', + nickName: '크리스티아누 호날두', + password: 'test1234', + createdAt: '2025-01-30T12:00:00', + }, + { + id: 3, + email: 'torres@test.com', + nickName: '페르난도 토레스', + password: '123456789', + createdAt: '2025-01-30T12:00:00', + }, +]; diff --git a/src/mock/service/auth/auth-utils.ts b/src/mock/service/auth/auth-utils.ts new file mode 100644 index 00000000..1755a060 --- /dev/null +++ b/src/mock/service/auth/auth-utils.ts @@ -0,0 +1,61 @@ +import type { LoginResponse, SignupResponse } from '@/types/service/auth'; + +import { AuthMockUser, authMockUsers } from './auth-mock'; + +const findUserByEmail = (email: string) => authMockUsers.find((user) => user.email === email); +const findUserByNickname = (nickName: string) => + authMockUsers.find((user) => user.nickName === nickName); + +const findUserByCredentials = (email: string, password: string) => + authMockUsers.find((user) => user.email === email && user.password === password); + +const createMockTokens = () => ({ + accessToken: 'mock-access-token', + refreshToken: 'mock-refresh-token', + tokenType: 'Bearer' as const, + expiresIn: 3600, +}); + +export const createLoginResponse = (email: string, password: string): LoginResponse => { + const user = findUserByCredentials(email, password); + if (!user) { + throw new Error('INVALID_CREDENTIALS'); + } + + const tokens = createMockTokens(); + + return { + ...tokens, + user: { + id: user.id, + email: user.email, + nickName: user.nickName, + }, + }; +}; + +export const createSignupResponse = ( + email: string, + nickName: string, + password: string, +): SignupResponse => { + const newUser: AuthMockUser = { + id: authMockUsers.length + 1, + email, + nickName, + password, + createdAt: new Date().toISOString(), + }; + + authMockUsers.push(newUser); + + return { + id: newUser.id, + email: newUser.email, + nickName: newUser.nickName, + createdAt: newUser.createdAt, + }; +}; + +export const isEmailTaken = (email: string) => !!findUserByEmail(email); +export const isNicknameTaken = (nickName: string) => !!findUserByNickname(nickName); diff --git a/src/types/service/auth.ts b/src/types/service/auth.ts new file mode 100644 index 00000000..32b9bc73 --- /dev/null +++ b/src/types/service/auth.ts @@ -0,0 +1,29 @@ +export interface SignupRequest { + email: string; + password: string; + nickName: string; +} + +export interface SignupResponse { + id: number; + email: string; + nickName: string; + createdAt: string; +} + +export interface LoginRequest { + email: string; + password: string; +} + +export interface LoginResponse { + accessToken: string; + refreshToken: string; + tokenType: 'Bearer'; + expiresIn: number; + user: { + id: number; + email: string; + nickName: string; + }; +} diff --git a/src/types/service/common.ts b/src/types/service/common.ts new file mode 100644 index 00000000..68eca4a0 --- /dev/null +++ b/src/types/service/common.ts @@ -0,0 +1,8 @@ +export interface CommonErrorResponse { + type: string; + title: string; + status: number; + detail: string; + instance: string; + errorCode?: string; +}