Skip to content

Commit

Permalink
feat: login (#13)
Browse files Browse the repository at this point in the history
* feat: 로그인, 회원가입 레이아웃 및 로그인 로직 구현

* feat: axios 에러 커스텀

* chore: 주석 추가

* feat: axios request interceptors 추가
  • Loading branch information
chaaerim committed Mar 5, 2023
1 parent a4ff6cb commit 1a9573a
Show file tree
Hide file tree
Showing 8 changed files with 394 additions and 134 deletions.
170 changes: 144 additions & 26 deletions src/apis/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,159 @@ import axios from 'axios';
const DEV_SERVER_URL = process.env.NEXT_PUBLIC_DEVELOPMENT_SERVER_URL;
const PROD_SERVER_URL = process.env.NEXT_PUBLIC_PRODUCTION_SERVER_URL;

const axiosClient = axios.create({
export const authToken = {
access: (() => {
try {
return localStorage.getItem('accessToken');
} catch (err) {
return null;
}
})(),
refresh: (() => {
try {
return localStorage.getItem('refreshToken');
} catch (err) {
return null;
}
})(),
};

export const axiosClient = axios.create({
baseURL: process.env.NODE_ENV === 'development' ? DEV_SERVER_URL : PROD_SERVER_URL,
headers: {
Authorization: `Bearer ${authToken.access}`,
'Content-Type': 'application/json; charset=utf-8',
},
});

const interceptorResponseFulfilled = (res: AxiosResponse) => {
if (200 <= res.status && res.status < 300) {
return res.data;
//에러 커스텀 로직
// effective-interview 에러 타입
export interface EffErrorResponse {
code: number;
message: string;
}

// 커스텀 에러 타입
export interface EffError extends AxiosError<EffErrorResponse> {
response: AxiosResponse<EffErrorResponse>;
isEffError: true;
message: string;
code: string;
status: number;
errorCode: number;
}

// effective-interview에서 정의한 에러인지 확인
export function isEffError(error: unknown): error is EffError {
try {
return (error as EffError).isEffError === true;
} catch {
return false;
}
}

return Promise.reject(res.data);
};
//axios 에러인지 확인
function isAxiosErrorWithResponseData(
error: unknown
): error is AxiosError & { response: AxiosResponse } {
try {
return axios.isAxiosError(error) && error.response?.data != null;
} catch {
return false;
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const interceptorResponseRejected = (error: AxiosError<any>) => {
return Promise.reject(new Error(error.response?.data?.message ?? error));
};
//axios에러를 커스텀한 에러로 변환
function createEffErrorFromAxiosError(error: AxiosError): EffErrorResponse | AxiosError {
if (isAxiosErrorWithResponseData(error)) {
const effError = error as EffError;

axiosClient.interceptors.response.use(interceptorResponseFulfilled, interceptorResponseRejected);
effError.isEffError = true;
effError.message = effError.response.data.message ?? '';
effError.errorCode = effError.response.data.code;
}

export const get = <T>(...args: Parameters<typeof axiosClient.get>) => {
return axiosClient.get<T, T>(...args);
};
return error;
}

export const post = <T>(...args: Parameters<typeof axiosClient.post>) => {
return axiosClient.post<T, T>(...args);
};
// 매 응답마다 axios error를 커스텀한 에러로 변환
axiosClient.interceptors.response.use(
response => response,
originalError =>
Promise.reject(
isAxiosErrorWithResponseData(originalError)
? createEffErrorFromAxiosError(originalError)
: originalError
)
);

export const put = <T>(...args: Parameters<typeof axiosClient.put>) => {
return axiosClient.put<T, T>(...args);
};
// 1. 요청 인터셉터
axiosClient.interceptors.request.use(
function (config) {
if (config?.headers == null) {
throw new Error(`config.header is undefined`);
}
config.headers['Content-Type'] = 'application/json; charset=utf-8';
config.headers['Authorization'] = authToken.access;

export const patch = <T>(...args: Parameters<typeof axiosClient.patch>) => {
return axiosClient.patch<T, T>(...args);
};
return config;
},
function (error) {
return Promise.reject(error);
}
);

export const del = <T>(...args: Parameters<typeof axiosClient.delete>) => {
return axiosClient.delete<T, T>(...args);
};
// 2. 응답 인터셉터
axiosClient.interceptors.response.use(
response => response.data,
async function (error: EffError) {
if (isEffError(error) && error.errorCode === 401) {
//refresh 토큰 처리 로직 추가
// redirectToLoginPage();
// redirect가 완료되고, API가 종료될 수 있도록 delay를 추가합니다.
await delay(500);
return null;
} else {
throw error;
}
}
);

function delay(time: number) {
return new Promise(res => setTimeout(res, time));
}

// const interceptorResponseFulfilled = (res: AxiosResponse) => {
// if (200 <= res.status && res.status < 300) {
// return res.data;
// }

// return Promise.reject(res.data);
// };

// // eslint-disable-next-line @typescript-eslint/no-explicit-any
// const interceptorResponseRejected = (error: AxiosError<any>) => {
// return Promise.reject(new Error(error.response?.data?.message ?? error));
// };

// axiosClient.interceptors.response.use(interceptorResponseFulfilled, interceptorResponseRejected);

// export const get = <T>(...args: Parameters<typeof axiosClient.get>) => {
// return axiosClient.get<T, T>(...args);
// };

// // export const post = <T>(...args: Parameters<typeof axiosClient.post>) => {
// // return axiosClient.post<T, T>(...args);
// // };

// export const put = <T>(...args: Parameters<typeof axiosClient.put>) => {
// return axiosClient.put<T, T>(...args);
// };

// export const patch = <T>(...args: Parameters<typeof axiosClient.patch>) => {
// return axiosClient.patch<T, T>(...args);
// };

// export const del = <T>(...args: Parameters<typeof axiosClient.delete>) => {
// return axiosClient.delete<T, T>(...args);
// };
25 changes: 17 additions & 8 deletions src/apis/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import axios from 'axios';

const axiosClient = axios.create({
baseURL: 'http://fteam-env-1.eba-ciibaeid.ap-northeast-2.elasticbeanstalk.com/api',
});
import { axiosClient } from './client';

export type LoginResponse = {
memberId: number;
accessToken: string;
refreshToken: string;
};
//signup and login apis
export async function postEmail(email: string) {
return await axiosClient.post('/v1/auth/email/send', { email });
return await axiosClient.post('/auth/email/send', { email });
}

export async function postEmailAndCode(email: string, verificationCode: number) {
return await axiosClient.post('/v1/auth/email/authenticate', { email, verificationCode });
return await axiosClient.post('/auth/email/authenticate', { email, verificationCode });
}

export async function postSignUp(email: string, password: string, confirmPassword: string) {
return await axiosClient.post('/v1/signup', { email, password, confirmPassword });
return await axiosClient.post('/signup', { email, password, confirmPassword });
}

export async function postLogin(email: string, password: string) {
const {
data: { memberId, accessToken, refreshToken },
} = await axiosClient.post<LoginResponse>('/auth/login', { email, password });
return { memberId, accessToken, refreshToken };
}
61 changes: 61 additions & 0 deletions src/hooks/form/useCheckLoginForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useCallback } from 'react';
import { useMutation } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';

import { postLogin } from '~/apis';
import { isEffError } from '~/apis/client';
import { emailPattern, passwordPattern } from '~/constants/validationPattern';

interface CheckLoginForm {
email: string;
password: string;
}

export const useCheckLoginForm = () => {
const {
register,
getValues,
formState: { isDirty, isValid },
} = useForm<CheckLoginForm>({ mode: 'onBlur' });

const isEmailPattern = useCallback(() => {
return {
value: emailPattern,
message: '올바른 이메일을 입력해주세요',
};
}, []);

const isPasswordPattern = useCallback(() => {
return {
value: passwordPattern,
message: '숫자와 한글만 입력해주세요.',
};
}, []);

const isDisabled = !isDirty || !isValid;

const isRequiredText = useCallback(
(text: string) => (text === '비밀번호' ? `${text}를 입력해주세요.` : `${text}을 입력해주세요.`),
[]
);

const { mutate: loginMutation } = useMutation(async () => {
const { email, password } = getValues();
try {
const data = await postLogin(email, password);
if (data) {
const { accessToken, refreshToken } = data;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
}
// 로컬스토리지에 제대로 aT, rT 들어갔으면 카테고리 선택 페이지로 라우팅
} catch (error: unknown) {
if (isEffError(error)) {
// TODO: toast 추가
console.log(error.message, error.errorCode);
}
}
});

return { register, isDisabled, isEmailPattern, isPasswordPattern, loginMutation, isRequiredText };
};
52 changes: 31 additions & 21 deletions src/hooks/form/useCheckSignUpForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useMutation } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';

import { postEmail, postEmailAndCode, postSignUp } from '~/apis';
import { isEffError } from '~/apis/client';
import { ConfirmModal } from '~/components/common/ConfirmModal';
import { emailPattern, passwordPattern } from '~/constants/validationPattern';

Expand All @@ -30,7 +31,10 @@ export const useCheckSignUpForm = () => {
const isDisabled = !isDirty || !isValid;

//validation check
const isRequiredText = useCallback((text: string) => `${text}을 입력해주세요.`, []);
const isRequiredText = useCallback(
(text: string) => (text === '비밀번호' ? `${text}를 입력해주세요.` : `${text}을 입력해주세요.`),
[]
);

const isMinLength = useCallback((minLength: number) => {
return {
Expand All @@ -56,44 +60,50 @@ export const useCheckSignUpForm = () => {
const isPasswordPattern = useCallback(() => {
return {
value: passwordPattern,
message: '숫자와 한글만 입력해주세요.',
message: '숫자와 영문만 입력해주세요.',
};
}, []);

// submit
const { mutate: createVerificationCode } = useMutation(async () => {
const { mutate: createVerificationCodeMutation } = useMutation(async () => {
const { email } = getValues();
try {
postEmail(email);
} catch (error: unknown) {
console.log(error);
if (isEffError(error)) {
// TODO: toast 추가
console.log(error.message, error.errorCode);
}
}
});

const { mutate: checkVerificationCode } = useMutation(async () => {
const { mutate: checkVerificationCodeMutation } = useMutation(async () => {
const { email, verificationCode } = getValues();

try {
const res = postEmailAndCode(email, verificationCode);
if (await res) {
router.push(`/signup/password/${email}`);
}
await postEmailAndCode(email, verificationCode);
router.push(`/signup/password/${email}`);
} catch (error: unknown) {
console.log(error);
if (isEffError(error)) {
// TODO: toast 추가
console.log(error.message, error.errorCode);
}
}
});

const { mutate: completeSignUp } = useMutation(async (email: string) => {
const { mutate: completeSignUpMutation } = useMutation(async (email: string) => {
const { password, confirmPassword } = getValues();
try {
const res = postSignUp(email, password, confirmPassword);
if (await res) {
await openModal({
children: <ConfirmModal title="회원가입 완료" subtitle="기술면접 연습을 시작해볼까요?" />,
});
}
await postSignUp(email, password, confirmPassword);

await openModal({
children: <ConfirmModal title="회원가입 완료" subtitle="기술면접 연습을 시작해볼까요?" />,
});
} catch (error: unknown) {
console.log(error);
if (isEffError(error)) {
// TODO: toast 추가
console.log(error.message, error.errorCode);
}
}
});

Expand All @@ -103,9 +113,9 @@ export const useCheckSignUpForm = () => {
setError,
isDisabled,
getValues,
createVerificationCode,
checkVerificationCode,
completeSignUp,
createVerificationCodeMutation,
checkVerificationCodeMutation,
completeSignUpMutation,
errors,
isRequiredText,
isMinLength,
Expand Down
4 changes: 3 additions & 1 deletion src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ export default function Home() {
<Spacing size={24} />
{/* 메인 화면 이미지 */}
<Spacing size={24} />
<Button onClick={onClick}>시작하기</Button>
<Button variant="largePrimary" onClick={onClick}>
시작하기
</Button>
</Flex.Center>
);
}
Loading

0 comments on commit 1a9573a

Please sign in to comment.