diff --git a/src/App.jsx b/src/App.jsx
index c41d189..d89f31f 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,7 +1,11 @@
import React from 'react';
+import { useDispatch } from 'react-redux';
import { Switch, Route } from 'react-router-dom';
+import { loadItem } from './services/storage';
+import { setUser } from './reducers/slice';
+
import MainPage from './pages/MainPage';
import WritePage from './pages/WritePage';
import IntroducePage from './pages/IntroducePage';
@@ -9,17 +13,29 @@ import LoginPage from './pages/LoginPage';
import RegisterPage from './pages/RegisterPage';
import HeaderContainer from './containers/common/HeaderContainer';
-const App = () => (
- <>
-
-
-
-
-
-
-
-
- >
-);
+const App = () => {
+ const dispatch = useDispatch();
+
+ const user = loadItem('user');
+
+ if (user) {
+ const { email } = user;
+
+ dispatch(setUser(email));
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+};
export default App;
diff --git a/src/App.test.jsx b/src/App.test.jsx
index d75e939..33b2479 100644
--- a/src/App.test.jsx
+++ b/src/App.test.jsx
@@ -1,17 +1,19 @@
import React from 'react';
-import { useDispatch, useSelector } from 'react-redux';
-
import { MemoryRouter } from 'react-router-dom';
+import { useDispatch, useSelector } from 'react-redux';
import { render } from '@testing-library/react';
import App from './App';
+import { loadItem } from './services/storage';
+
import STUDY_GROUPS from '../fixtures/study-groups';
import STUDY_GROUP from '../fixtures/study-group';
jest.mock('react-redux');
+jest.mock('./services/storage');
describe('App', () => {
const dispatch = jest.fn();
@@ -87,4 +89,23 @@ describe('App', () => {
expect(container).toHaveTextContent('회원가입');
});
});
+
+ context('when logged in', () => {
+ const user = {
+ email: 'seungmin@naver.com',
+ };
+
+ beforeEach(() => {
+ loadItem.mockImplementation(() => user);
+ });
+
+ it('calls dispatch with "setUser" action', () => {
+ renderApp({ path: '/' });
+
+ expect(dispatch).toBeCalledWith({
+ type: 'application/setUser',
+ payload: user.email,
+ });
+ });
+ });
});
diff --git a/src/components/common/Header.jsx b/src/components/common/Header.jsx
index eaa9008..762f183 100644
--- a/src/components/common/Header.jsx
+++ b/src/components/common/Header.jsx
@@ -43,15 +43,21 @@ const Spacer = styled.div`
height: 6rem;
`;
-const Header = () => (
+const Header = ({ user }) => (
<>
제목(미정)
-
- 로그인
- 회원가입
-
+ {user ? (
+
+
+
+ ) : (
+
+ 로그인
+ 회원가입
+
+ )}
diff --git a/src/containers/auth/LoginFormContainer.jsx b/src/containers/auth/LoginFormContainer.jsx
index 3d450e0..1a7e44b 100644
--- a/src/containers/auth/LoginFormContainer.jsx
+++ b/src/containers/auth/LoginFormContainer.jsx
@@ -1,16 +1,20 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, useEffect } from 'react';
+import { useHistory } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { get } from '../../util/utils';
-import { changeAuthField } from '../../reducers/slice';
+import { changeAuthField, clearAuth, requestLogin } from '../../reducers/slice';
import AuthForm from '../../components/auth/AuthForm';
const LoginFormContainer = () => {
const dispatch = useDispatch();
+ const history = useHistory();
const login = useSelector(get('login'));
+ const user = useSelector(get('user'));
+ const authError = useSelector(get('authError'));
const onChangeLoginField = useCallback(({ name, value }) => {
dispatch(
@@ -22,11 +26,33 @@ const LoginFormContainer = () => {
);
});
+ const onSubmit = useCallback(() => {
+ // TODO: 로그인 validation 체크 로직 추가
+
+ dispatch(requestLogin());
+ }, [dispatch]);
+
+ useEffect(() => {
+ if (user) {
+ history.push('/');
+ }
+
+ if (authError) {
+ // TODO: error 처리 추가
+ console.error(authError);
+ }
+ }, [user, authError]);
+
+ useEffect(() => () => {
+ dispatch(clearAuth());
+ }, [dispatch]);
+
return (
);
};
diff --git a/src/containers/auth/LoginFormContainer.test.jsx b/src/containers/auth/LoginFormContainer.test.jsx
index 08a3495..801007c 100644
--- a/src/containers/auth/LoginFormContainer.test.jsx
+++ b/src/containers/auth/LoginFormContainer.test.jsx
@@ -6,11 +6,22 @@ import { render, fireEvent } from '@testing-library/react';
import LoginFormContainer from './LoginFormContainer';
+const mockPush = jest.fn();
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useHistory() {
+ return { push: mockPush };
+ },
+}));
+
describe('LoginFormContainer', () => {
const dispatch = jest.fn();
beforeEach(() => {
dispatch.mockClear();
+ mockPush.mockClear();
+
useDispatch.mockImplementation(() => dispatch);
useSelector.mockImplementation((selector) => selector({
@@ -18,6 +29,8 @@ describe('LoginFormContainer', () => {
userEmail: '',
password: '',
},
+ user: given.user,
+ authError: given.authError,
}));
});
@@ -59,5 +72,38 @@ describe('LoginFormContainer', () => {
});
});
});
+
+ it('submit event calls dispatch', () => {
+ const { getByTestId } = renderLoginFormContainer();
+
+ const button = getByTestId('auth-button');
+
+ expect(button).not.toBeNull();
+
+ fireEvent.submit(button);
+
+ expect(dispatch).toBeCalled();
+ });
+ });
+
+ describe('actions after login', () => {
+ context('when success auth to login', () => {
+ given('user', () => ({
+ user: 'seungmin@naver.com',
+ }));
+
+ it('redirection go to main page', () => {
+ renderLoginFormContainer();
+
+ expect(mockPush).toBeCalledWith('/');
+ });
+ });
+
+ // TODO: 현재 authError는 콘솔 출력
+ context('when failure auth to login', () => {
+ given('authError', () => ({
+ authError: 'error',
+ }));
+ });
});
});
diff --git a/src/containers/auth/RegisterFormContainer.jsx b/src/containers/auth/RegisterFormContainer.jsx
index 036d1e9..8a2954a 100644
--- a/src/containers/auth/RegisterFormContainer.jsx
+++ b/src/containers/auth/RegisterFormContainer.jsx
@@ -14,6 +14,7 @@ const RegisterFormContainer = () => {
const register = useSelector(get('register'));
const auth = useSelector(get('auth'));
+ const user = useSelector(get('user'));
const authError = useSelector(get('authError'));
const onChangeRegisterField = useCallback(({ name, value }) => {
@@ -27,23 +28,31 @@ const RegisterFormContainer = () => {
}, [dispatch]);
const onSubmit = useCallback(() => {
+ // TODO: 회원가입 validation 체크 로직 추가
+
dispatch(requestRegister());
}, [dispatch]);
+ useEffect(() => {
+ if (user) {
+ history.push('/');
+ }
+ }, [user]);
+
useEffect(() => {
if (auth) {
history.push('/login');
}
if (authError) {
- // TODO: 추 후 error 처리
+ // TODO: error 처리 추가
console.error(authError);
}
+ }, [auth, authError]);
- return () => {
- dispatch(clearAuth());
- };
- }, [dispatch, auth, authError]);
+ useEffect(() => () => {
+ dispatch(clearAuth());
+ }, [dispatch]);
return (
{
beforeEach(() => {
dispatch.mockClear();
+ mockPush.mockClear();
+
useDispatch.mockImplementation(() => dispatch);
useSelector.mockImplementation((selector) => selector({
+ user: given.user,
+ auth: given.auth,
+ authError: given.authError,
register: {
userEmail: '',
password: '',
passwordConfirm: '',
},
- auth: given.auth,
- authError: given.authError,
}));
});
@@ -90,7 +93,7 @@ describe('RegisterFormContainer', () => {
});
});
- describe('action after signing up', () => {
+ describe('actions after signing up', () => {
context('when success auth to register', () => {
given('auth', () => ({
auth: 'seungmin@naver.com',
@@ -110,4 +113,16 @@ describe('RegisterFormContainer', () => {
}));
});
});
+
+ describe('action after login', () => {
+ given('user', () => ({
+ user: 'seungmin@naver.com',
+ }));
+
+ it('redirection go to main page', () => {
+ renderRegisterFormContainer();
+
+ expect(mockPush).toBeCalledWith('/');
+ });
+ });
});
diff --git a/src/containers/common/HeaderContainer.jsx b/src/containers/common/HeaderContainer.jsx
index 108fc19..c711dc3 100644
--- a/src/containers/common/HeaderContainer.jsx
+++ b/src/containers/common/HeaderContainer.jsx
@@ -1,10 +1,19 @@
import React from 'react';
+import { useSelector } from 'react-redux';
+
+import { get } from '../../util/utils';
+
import Header from '../../components/common/Header';
-const HeaderContainer = () => ((
-
-)
-);
+const HeaderContainer = () => {
+ const user = useSelector(get('user'));
+
+ return (
+
+ );
+};
export default HeaderContainer;
diff --git a/src/reducers/slice.js b/src/reducers/slice.js
index dedcf5b..4d6e757 100644
--- a/src/reducers/slice.js
+++ b/src/reducers/slice.js
@@ -6,8 +6,10 @@ import {
getStudyGroup,
getStudyGroups,
postStudyGroup,
+ postUserLogin,
postUserRegister,
} from '../services/api';
+import { saveItem } from '../services/storage';
const writeInitialState = {
title: '',
@@ -37,11 +39,12 @@ const { actions, reducer } = createSlice({
groups: [],
group: null,
groupId: null,
+ user: null,
+ auth: null,
+ authError: null,
writeField: writeInitialState,
register: authInitialState.register,
login: authInitialState.login,
- auth: null,
- authError: null,
},
reducers: {
@@ -109,10 +112,10 @@ const { actions, reducer } = createSlice({
};
},
- setAuth(state, { payload: user }) {
+ setAuth(state, { payload: auth }) {
return {
...state,
- auth: user,
+ auth,
};
},
@@ -130,6 +133,13 @@ const { actions, reducer } = createSlice({
authError: null,
};
},
+
+ setUser(state, { payload: user }) {
+ return {
+ ...state,
+ user,
+ };
+ },
},
});
@@ -144,6 +154,7 @@ export const {
setAuth,
setAuthError,
clearAuth,
+ setUser,
} = actions;
export const loadStudyGroups = (tag) => async (dispatch) => {
@@ -188,4 +199,23 @@ export const requestRegister = () => async (dispatch, getState) => {
}
};
+export const requestLogin = () => async (dispatch, getState) => {
+ const { login: { userEmail, password } } = getState();
+
+ try {
+ const { user } = await postUserLogin({ userEmail, password });
+
+ const { email } = user;
+
+ saveItem('user', {
+ email,
+ });
+
+ dispatch(setUser(email));
+ dispatch(clearAuthFields());
+ } catch (error) {
+ setAuthError(error);
+ }
+};
+
export default reducer;
diff --git a/src/reducers/slice.test.js b/src/reducers/slice.test.js
index 96e4e62..dc41840 100644
--- a/src/reducers/slice.test.js
+++ b/src/reducers/slice.test.js
@@ -18,12 +18,14 @@ import reducer, {
setAuthError,
clearAuth,
requestRegister,
+ requestLogin,
+ setUser,
} from './slice';
import STUDY_GROUPS from '../../fixtures/study-groups';
import STUDY_GROUP from '../../fixtures/study-group';
import WRITE_FORM from '../../fixtures/write-form';
-import { postUserRegister } from '../services/api';
+import { postUserLogin, postUserRegister } from '../services/api';
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
@@ -36,6 +38,9 @@ describe('reducer', () => {
groups: [],
group: null,
groupId: null,
+ user: null,
+ auth: null,
+ authError: null,
writeField: {
title: '',
contents: '',
@@ -54,8 +59,6 @@ describe('reducer', () => {
userEmail: '',
password: '',
},
- auth: null,
- authError: null,
};
it('returns initialState', () => {
@@ -261,13 +264,27 @@ describe('reducer', () => {
authError: 'error',
};
- it('Clean up to auth and authError', () => {
+ it('clean up to auth and authError', () => {
const { auth, authError } = reducer(initialState, clearAuth());
expect(auth).toBe(null);
expect(authError).toBe(null);
});
});
+
+ describe('setUser', () => {
+ const initialState = {
+ user: null,
+ };
+
+ const userEmail = 'seungmin@naver.com';
+
+ it('success login', () => {
+ const { user } = reducer(initialState, setUser(userEmail));
+
+ expect(user).toBe(userEmail);
+ });
+ });
});
describe('async actions', () => {
@@ -342,6 +359,7 @@ describe('async actions', () => {
email: userEmail,
},
}));
+
it('dispatches requestRegister action success to return user email', async () => {
await store.dispatch(requestRegister());
@@ -368,4 +386,52 @@ describe('async actions', () => {
});
});
});
+
+ describe('requestLogin', () => {
+ const login = {
+ userEmail: 'seungmin@naver.com',
+ password: '123456',
+ };
+
+ beforeEach(() => {
+ store = mockStore({
+ login,
+ });
+ });
+
+ context('without auth error', () => {
+ const { userEmail } = login;
+
+ postUserLogin.mockImplementationOnce(() => ({
+ user: {
+ email: userEmail,
+ },
+ }));
+
+ it('dispatches requestLogin action success to return user email', async () => {
+ await store.dispatch(requestLogin());
+
+ const actions = store.getActions();
+
+ expect(actions[0]).toEqual(setUser(userEmail));
+ expect(actions[1]).toEqual(clearAuthFields());
+ });
+ });
+
+ context('with auth error', () => {
+ postUserLogin.mockImplementationOnce(() => {
+ throw new Error('error');
+ });
+
+ it('dispatches requestLogin action failure to return error', async () => {
+ try {
+ await store.dispatch(requestLogin());
+ } catch (error) {
+ const actions = store.getActions();
+
+ expect(actions[0]).toEqual(setAuthError(error));
+ }
+ });
+ });
+ });
});
diff --git a/src/services/__mocks__/api.js b/src/services/__mocks__/api.js
index 2dee810..4f4819b 100644
--- a/src/services/__mocks__/api.js
+++ b/src/services/__mocks__/api.js
@@ -5,3 +5,5 @@ export const getStudyGroup = async () => {};
export const postStudyGroup = async () => {};
export const postUserRegister = jest.fn();
+
+export const postUserLogin = jest.fn();
diff --git a/src/services/__mocks__/storage.js b/src/services/__mocks__/storage.js
new file mode 100644
index 0000000..3135add
--- /dev/null
+++ b/src/services/__mocks__/storage.js
@@ -0,0 +1,9 @@
+const saveItem = jest.fn();
+const loadItem = jest.fn();
+const removeItem = jest.fn();
+
+export {
+ saveItem,
+ loadItem,
+ removeItem,
+};
diff --git a/src/services/storage.js b/src/services/storage.js
new file mode 100644
index 0000000..ca53a99
--- /dev/null
+++ b/src/services/storage.js
@@ -0,0 +1,11 @@
+export const saveItem = (key, value) => {
+ localStorage.setItem(key, JSON.stringify(value));
+};
+
+export const loadItem = (key) => {
+ const user = localStorage.getItem(key);
+
+ return JSON.parse(user);
+};
+
+export const removeItem = (key) => localStorage.removeItem(key);
diff --git a/src/services/storage.test.js b/src/services/storage.test.js
new file mode 100644
index 0000000..bd8f97d
--- /dev/null
+++ b/src/services/storage.test.js
@@ -0,0 +1,44 @@
+import { saveItem, loadItem, removeItem } from './storage';
+
+describe('storage', () => {
+ jest.spyOn(window.localStorage.__proto__, 'setItem');
+ jest.spyOn(window.localStorage.__proto__, 'getItem');
+
+ beforeEach(() => {
+ const mockStorage = {};
+
+ window.localStorage = {
+ setItem: (key, val) => Object.assign(mockStorage, { [key]: val }),
+ getItem: (key) => mockStorage[key],
+ };
+
+ window.localStorage.__proto__.removeItem = jest.fn();
+ });
+
+ describe('saveItem', () => {
+ const value = {
+ value: 'value',
+ };
+ it('calls localStorage setItem', () => {
+ saveItem('key', value);
+
+ expect(localStorage.setItem).toBeCalledWith('key', JSON.stringify(value));
+ });
+ });
+
+ describe('loadItem', () => {
+ it('calls localStorage getItem', () => {
+ loadItem('key');
+
+ expect(localStorage.getItem).toBeCalledWith('key');
+ });
+ });
+
+ describe('removeItem', () => {
+ it('calls localStorage removeItem', () => {
+ removeItem('key');
+
+ expect(localStorage.removeItem).toBeCalled();
+ });
+ });
+});