Skip to content

Commit

Permalink
Merge pull request #167 from woowacourse-teams/feature/#149
Browse files Browse the repository at this point in the history
로그인 기능 개발
  • Loading branch information
cys4585 authored Aug 1, 2024
2 parents 058d776 + 32cebc2 commit 6b9ac55
Show file tree
Hide file tree
Showing 15 changed files with 299 additions and 50 deletions.
1 change: 1 addition & 0 deletions frontend/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const config: StorybookConfig = {
'@_pages': path.resolve(__dirname, '../src/pages'),
'@_types': path.resolve(__dirname, '../src/types'),
'@_utils': path.resolve(__dirname, '../src/utils'),
'@_routes': path.resolve(__dirname, 'src/routes'),
};
}

Expand Down
1 change: 1 addition & 0 deletions frontend/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const config: Config = {
'^@_pages/(.*)$': '<rootDir>/src/pages/$1',
'^@_types/(.*)$': '<rootDir>/src/types/$1',
'^@_utils/(.*)$': '<rootDir>/src/utils/$1',
'^@_routes/(.*)$': '<rootDir>/src/routes/$1',
},
};

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { RouterProvider } from 'react-router-dom';
import createQueryClient from './queryClient';
import fonts from '@_common/font.style';
import reset from './common/reset.style';
import router from './router';
import { useMemo } from 'react';
import { theme } from '@_common/theme/theme.style';
import router from '@_routes/router';

export default function App() {
const queryClient = useMemo(createQueryClient, []);
Expand Down
163 changes: 163 additions & 0 deletions frontend/src/apis/apiClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { getToken } from '@_utils/tokenManager';

type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';

const DEFAULT_HEADERS = {
'Content-Type': 'application/json',
};

const BASE_URL = `${process.env.BASE_URL}/v1`;

function addBaseUrl(endpoint: string) {
if (endpoint[0] !== '/') endpoint = '/' + endpoint;
return BASE_URL + endpoint;
}

function getHeaders(token?: string) {
const headers = new Headers(DEFAULT_HEADERS);
if (token) {
headers.append('Authorization', `Bearer ${token}`);
}
return headers;
}

async function request(
method: Method,
endpoint: string,
data: object = {},
config: RequestInit = {},
isRequiredAuth: boolean = false,
) {
const url = addBaseUrl(endpoint);
const token = isRequiredAuth ? getToken() : undefined;

const options: RequestInit = {
method,
headers: getHeaders(token),
...config,
};

if (method !== 'GET') {
options.body = JSON.stringify(data);
}

return await fetch(url, options);
}

async function get(
endpoint: string,
config: RequestInit = {},
isRequiredAuth: boolean = false,
) {
return request('GET', endpoint, {}, config, isRequiredAuth);
}

async function post(
endpoint: string,
data: object = {},
config: RequestInit = {},
isRequiredAuth: boolean = false,
) {
return request('POST', endpoint, data, config, isRequiredAuth);
}

async function put(
endpoint: string,
data: object = {},
config: RequestInit = {},
isRequiredAuth: boolean = false,
) {
return request('PUT', endpoint, data, config, isRequiredAuth);
}

async function patch(
endpoint: string,
data: object = {},
config: RequestInit = {},
isRequiredAuth: boolean = false,
) {
return request('PATCH', endpoint, data, config, isRequiredAuth);
}

/**
* delete는 예약어로 사용할 수 없는 함수 이름이라 부득이 `deleteMethod`로 이름을 지었습니다.
*/
async function deleteMethod(
endpoint: string,
data: object = {},
config: RequestInit = {},
isRequiredAuth: boolean = false,
) {
return request('DELETE', endpoint, data, config, isRequiredAuth);
}

const ApiClient = {
async getWithoutAuth(endpoint: string, config: RequestInit = {}) {
return get(endpoint, config, false);
},
async getWithAuth(endpoint: string, config: RequestInit = {}) {
return get(endpoint, config, true);
},

async postWithAuth(
endpoint: string,
data: object = {},
config: RequestInit = {},
) {
return post(endpoint, data, config, true);
},
async postWithoutAuth(
endpoint: string,
data: object = {},
config: RequestInit = {},
) {
return post(endpoint, data, config, false);
},

async putWithAuth(
endpoint: string,
data: object = {},
config: RequestInit = {},
) {
return put(endpoint, data, config, true);
},
async putWithoutAuth(
endpoint: string,
data: object = {},
config: RequestInit = {},
) {
return put(endpoint, data, config, false);
},

async patchWithAuth(
endpoint: string,
data: object = {},
config: RequestInit = {},
) {
return patch(endpoint, data, config, true);
},
async patchWithoutAuth(
endpoint: string,
data: object = {},
config: RequestInit = {},
) {
return patch(endpoint, data, config, false);
},

async deleteWithAuth(
endpoint: string,
data: object = {},
config: RequestInit = {},
) {
return deleteMethod(endpoint, data, config, true);
},
async deleteWithoutAuth(
endpoint: string,
data: object = {},
config: RequestInit = {},
) {
return deleteMethod(endpoint, data, config, false);
},
};

export default ApiClient;
11 changes: 11 additions & 0 deletions frontend/src/apis/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import ApiClient from './apiClient';
import { checkStatus } from './apiconfig';

export const login = async (loginInputInfo: { nickname: string }) => {
const response = await ApiClient.postWithoutAuth(
'auth/login',
loginInputInfo,
);
checkStatus(response);
return response.json();
};
1 change: 1 addition & 0 deletions frontend/src/apis/endPoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ const getEndpoint = (string: string) => {
const ENDPOINTS = {
moim: getEndpoint('v1/moim'),
moims: getEndpoint('v1/moim'),
auth: getEndpoint('v1/auth'),
};
export default ENDPOINTS;
26 changes: 9 additions & 17 deletions frontend/src/apis/gets.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,21 @@
import ENDPOINTS from '@_apis/endPoints';
import { GetMoim, GetMoims } from '@_apis/responseTypes';
// ./src/apis/gets.ts
import { MoimInfo } from '@_types/index';
import { checkStatus, defaultOptions } from './apiconfig';
import ApiClient from './apiClient';
import { GetMoim, GetMoims } from './responseTypes';
import { checkStatus } from './apiconfig';

const defaultGetOptions = {
method: 'GET',
...defaultOptions,
};
export const getMoims = async (): Promise<MoimInfo[]> => {
const url = ENDPOINTS.moims;

const response = await fetch(url, defaultGetOptions);

const response = await ApiClient.getWithAuth('moim');
checkStatus(response);
const json = (await response.json()) as GetMoims;

const json: GetMoims = await response.json();
return json.data.moims;
};

export const getMoim = async (moimId: number): Promise<MoimInfo> => {
const url = `${ENDPOINTS.moim}/${moimId}`;

const response = await fetch(url, defaultGetOptions);

const response = await ApiClient.getWithAuth(`moim/${moimId}`);
checkStatus(response);

const json = (await response.json()) as GetMoim;
const json: GetMoim = await response.json();
return json.data;
};
34 changes: 9 additions & 25 deletions frontend/src/apis/posts.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,21 @@
import { checkStatus, defaultOptions } from './apiconfig';

import ENDPOINTS from '@_apis/endPoints';
import { MoimInputInfo } from '@_types/index';
import { PostMoim } from '@_apis/responseTypes';
import ApiClient from './apiClient';
import { PostMoim } from './responseTypes';
import { checkStatus } from './apiconfig';

const defaultPostOptions = {
method: 'POST',
...defaultOptions,
};
export const postMoim = async (moim: MoimInputInfo): Promise<number> => {
const url = ENDPOINTS.moim;

const options = {
...defaultPostOptions,
body: JSON.stringify(moim),
};

const response = await fetch(url, options);
const response = await ApiClient.postWithAuth('moim', moim);

checkStatus(response);

const json = (await response.json()) as PostMoim;
const json: PostMoim = await response.json();
return json.data;
};

export const postJoinMoim = async (moimId: number, nickname: string) => {
const url = `${ENDPOINTS.moims}/join`;
const options = {
...defaultPostOptions,
body: JSON.stringify({ moimId, nickname }),
};

const response = await fetch(url, options);

const response = await ApiClient.postWithAuth('moim/join', {
moimId,
nickname,
});
await checkStatus(response);
};
39 changes: 35 additions & 4 deletions frontend/src/components/LoginForm/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,46 @@
import Button from '@_components/Button/Button';
import LabeledInput from '@_components/Input/MoimInput';
import * as S from './LoginForm.style';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import ROUTES from '@_constants/routes';
import { setToken } from '@_utils/tokenManager';
import { login } from '@_apis/auth';

// TODO: 로그인 기능 요구사항 변경 예정
export default function LoginForm() {
const navigate = useNavigate();

const [nickname, setNickname] = useState('');
const [isValid, setIsValid] = useState(false);

const handleNicknameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNickname(e.target.value);
};

const handleLoginFormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

const response = await login({ nickname });
console.log(response);
setToken(response.data.accessToken);
navigate(ROUTES.main);
};

useEffect(() => {
setIsValid(nickname.length > 0);
}, [nickname]);

return (
<form css={S.formContainerStyle}>
<form css={S.formContainerStyle} onSubmit={handleLoginFormSubmit}>
<div css={S.inputContainerStyle}>
<LabeledInput title="아이디" />
<LabeledInput title="비밀번호" />
<LabeledInput
value={nickname}
title="닉네임"
onChange={handleNicknameChange}
/>
</div>
<Button shape="bar" onClick={() => {}} disabled>
<Button shape="bar" onClick={() => {}} disabled={!isValid}>
로그인
</Button>
</form>
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/routes/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import ROUTES from '@_constants/routes';
import { checkAuthentication } from '@_utils/checkAuthentication';
import { PropsWithChildren } from 'react';
import { Navigate, useLocation } from 'react-router-dom';

export default function ProtectedRoute(props: PropsWithChildren) {
const { children } = props;

const isAuthenticated = checkAuthentication();
const location = useLocation();

return isAuthenticated ? (
children
) : (
<Navigate to={ROUTES.login} state={{ from: location }} replace />
);
}
Loading

0 comments on commit 6b9ac55

Please sign in to comment.