Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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: authService,
Copy link
Member

@Chiman2937 Chiman2937 Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금보니까 제가 작업을 잘못했네요 😭
authService 키와 값이 동일하므로 아래와 같이 변경하면 되겠네용

Suggested change
usersService: userService,
authService: authService,
usersService,
authService,

userService는 추후에 제가 수정하겠습니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

};
};

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,10 @@
'use client';

import { useRouter } from 'next/navigation';

import { type AnyFieldApi, useForm } from '@tanstack/react-form';

import { authServiceRemote, isProblemDetailError } from '@/api/service';
import { FormInput } from '@/components/shared';
import { Button } from '@/components/ui';
import { loginSchema } from '@/lib/schema/auth';
Expand All @@ -19,6 +22,9 @@ const getHintMessage = (field: AnyFieldApi) => {
};

export const LoginForm = () => {
const router = useRouter();
const { login } = authServiceRemote();

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 login(payload);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 src/api/index.ts 에서 아래와 같이 export 하고 있기 때문에

export const API = provideAPIService();
Suggested change
const result = await login(payload);
const result = API.authService.login(payload);

이렇게 사용하시면 됩니다!

그러면 아래 구문이 필요없어지거든요

const { login } = authServiceRemote();

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,10 @@
'use client';

import { useRouter } from 'next/navigation';

import { type AnyFieldApi, useForm } from '@tanstack/react-form';

import { authServiceRemote, isProblemDetailError } from '@/api/service';
import { FormInput } from '@/components/shared';
import { Button } from '@/components/ui';
import { signupSchema } from '@/lib/schema/auth';
Expand All @@ -19,6 +22,9 @@ const getHintMessage = (field: AnyFieldApi) => {
};

export const SignupForm = () => {
const router = useRouter();
const { signup } = authServiceRemote();

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 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;
}