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: 3 additions & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
@@ -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,
};
};

Expand Down
33 changes: 33 additions & 0 deletions src/api/service/auth-service/index.ts
Original file line number Diff line number Diff line change
@@ -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<CommonErrorResponse> => {
return (
typeof error === 'object' &&
error !== null &&
'isAxiosError' in error &&
(error as AxiosError).isAxiosError === true
);
};

export const authServiceRemote = () => ({
// 로그인
login: async (payload: LoginRequest): Promise<LoginResponse> => {
const { data } = await baseAPI.post<LoginResponse>('/api/v1/auth/login', payload);
return data;
},

// 회원가입
signup: async (payload: SignupRequest): Promise<SignupResponse> => {
const { data } = await baseAPI.post<SignupResponse>('/api/v1/auth/signup', payload);
return data;
},

// 로그아웃
logout: async (): Promise<void> => {
await baseAPI.post('/api/v1/auth/logout');
},
});
1 change: 1 addition & 0 deletions src/api/service/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './auth-service';
export * from './user-service';
32 changes: 29 additions & 3 deletions src/components/pages/login/login-form/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -19,6 +23,8 @@ const getHintMessage = (field: AnyFieldApi) => {
};

export const LoginForm = () => {
const router = useRouter();

const form = useForm({
defaultValues: {
email: '',
Expand All @@ -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('알 수 없는 오류가 발생했습니다.');
}
}
},
});

Expand Down
33 changes: 30 additions & 3 deletions src/components/pages/signup/signup-form/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -19,6 +23,8 @@ const getHintMessage = (field: AnyFieldApi) => {
};

export const SignupForm = () => {
const router = useRouter();

const form = useForm({
defaultValues: {
email: '',
Expand All @@ -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('알 수 없는 오류가 발생했습니다.');
}
}
},
});

Expand Down
3 changes: 2 additions & 1 deletion src/mock/handlers.ts
Original file line number Diff line number Diff line change
@@ -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];
66 changes: 66 additions & 0 deletions src/mock/service/auth/auth-handlers.ts
Original file line number Diff line number Diff line change
@@ -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<CommonErrorResponse>(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<CommonErrorResponse>(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<LoginResponse>(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];
31 changes: 31 additions & 0 deletions src/mock/service/auth/auth-mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export interface AuthMockUser {
id: number;
email: string;
nickName: string;
password: string;
createdAt: string;
}

export const authMockUsers: AuthMockUser[] = [
{
id: 1,
email: '[email protected]',
nickName: '리오넬 메시',
password: 'test9876',
createdAt: '2025-01-30T12:00:00',
},
{
id: 2,
email: '[email protected]',
nickName: '크리스티아누 호날두',
password: 'test1234',
createdAt: '2025-01-30T12:00:00',
},
{
id: 3,
email: '[email protected]',
nickName: '페르난도 토레스',
password: '123456789',
createdAt: '2025-01-30T12:00:00',
},
];
61 changes: 61 additions & 0 deletions src/mock/service/auth/auth-utils.ts
Original file line number Diff line number Diff line change
@@ -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);
29 changes: 29 additions & 0 deletions src/types/service/auth.ts
Original file line number Diff line number Diff line change
@@ -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;
};
}
8 changes: 8 additions & 0 deletions src/types/service/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface CommonErrorResponse {
type: string;
title: string;
status: number;
detail: string;
instance: string;
errorCode?: string;
}