diff --git a/.eslintrc.js b/.eslintrc.js
index e225af8..65a726b 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -52,5 +52,6 @@ module.exports = {
'react/prop-types': 'off',
'linebreak-style': 'off',
'no-proto': 'off',
+ 'no-param-reassign': ['error', { props: true, ignorePropertyModificationsFor: ['draft'] }],
},
};
diff --git a/package-lock.json b/package-lock.json
index f220b1e..25f7e56 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2159,6 +2159,13 @@
"redux": "^4.0.0",
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0"
+ },
+ "dependencies": {
+ "immer": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-7.0.15.tgz",
+ "integrity": "sha512-yM7jo9+hvYgvdCQdqvhCNRRio0SCXc8xDPzA25SvKWa7b1WVPjLwQs1VYU5JPXjcJPTqAa5NP5dqpORGYBQ2AA=="
+ }
}
},
"@sideway/address": {
@@ -7478,9 +7485,9 @@
"dev": true
},
"immer": {
- "version": "7.0.14",
- "resolved": "https://registry.npmjs.org/immer/-/immer-7.0.14.tgz",
- "integrity": "sha512-BxCs6pJwhgSEUEOZjywW7OA8DXVzfHjkBelSEl0A+nEu0+zS4cFVdNOONvt55N4WOm8Pu4xqSPYxhm1Lv2iBBA=="
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.0.tgz",
+ "integrity": "sha512-jm87NNBAIG4fHwouilCHIecFXp5rMGkiFrAuhVO685UnMAlOneEAnOyzPt8OnP47TC11q/E7vpzZe0WvwepFTg=="
},
"immutable": {
"version": "3.7.6",
diff --git a/package.json b/package.json
index 276c49d..ef00585 100644
--- a/package.json
+++ b/package.json
@@ -31,18 +31,19 @@
"draft-js": "^0.11.7",
"draftjs-to-html": "^0.9.1",
"firebase": "^8.1.1",
+ "immer": "^8.0.0",
"moment": "^2.29.1",
"moment-timezone": "^0.5.32",
"qs": "^6.9.4",
"react": "^17.0.1",
- "redux-devtools-extension": "^2.13.8",
"react-dom": "^17.0.1",
"react-draft-wysiwyg": "^1.14.5",
- "redux-logger": "^3.0.6",
"react-moment": "^1.0.0",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
- "redux": "^4.0.5"
+ "redux": "^4.0.5",
+ "redux-devtools-extension": "^2.13.8",
+ "redux-logger": "^3.0.6"
},
"devDependencies": {
"@babel/core": "^7.12.3",
diff --git a/src/App.test.jsx b/src/App.test.jsx
index 7a1393d..d75e939 100644
--- a/src/App.test.jsx
+++ b/src/App.test.jsx
@@ -27,6 +27,15 @@ describe('App', () => {
writeField: {
tags: [],
},
+ register: {
+ userEmail: '',
+ password: '',
+ passwordConfirm: '',
+ },
+ login: {
+ userEmail: '',
+ password: '',
+ },
}));
});
@@ -67,7 +76,7 @@ describe('App', () => {
it('renders the study login page', () => {
const { container } = renderApp({ path: '/login' });
- expect(container).toHaveTextContent('Login');
+ expect(container).toHaveTextContent('로그인');
});
});
@@ -75,7 +84,7 @@ describe('App', () => {
it('renders the study register page', () => {
const { container } = renderApp({ path: '/register' });
- expect(container).toHaveTextContent('Register');
+ expect(container).toHaveTextContent('회원가입');
});
});
});
diff --git a/src/components/auth/AuthForm.jsx b/src/components/auth/AuthForm.jsx
index 66679ef..cb0c1a8 100644
--- a/src/components/auth/AuthForm.jsx
+++ b/src/components/auth/AuthForm.jsx
@@ -1,20 +1,58 @@
import React from 'react';
import styled from '@emotion/styled';
+
import Responsive from '../../styles/Responsive';
const AuthFormWrapper = styled(Responsive)``;
-const AuthForm = ({ type }) => (
-
- {type}
-
-
- {type === 'Register' && (
-
- )}
-
-
-);
+const FORM_TYPE = {
+ login: '로그인',
+ register: '회원가입',
+};
+
+const AuthForm = ({ type, onChange, fields }) => {
+ const formType = FORM_TYPE[type];
+
+ const { userEmail, password } = fields;
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+
+ onChange({ name, value });
+ };
+
+ return (
+
+ {formType}
+
+
+ {type === 'register' && (
+
+ )}
+
+ );
+};
export default AuthForm;
diff --git a/src/components/auth/AuthForm.test.jsx b/src/components/auth/AuthForm.test.jsx
index fdf6256..48b0665 100644
--- a/src/components/auth/AuthForm.test.jsx
+++ b/src/components/auth/AuthForm.test.jsx
@@ -1,40 +1,98 @@
import React from 'react';
-import { render } from '@testing-library/react';
+import { render, fireEvent } from '@testing-library/react';
import AuthForm from './AuthForm';
describe('AuthForm', () => {
- const renderAuthForm = ({ type }) => render((
-
+ const handleChange = jest.fn();
+
+ beforeEach(() => {
+ handleChange.mockClear();
+ });
+
+ const renderAuthForm = ({ type, fields }) => render((
+
));
context('when type is login', () => {
const login = {
- type: 'Login',
+ type: 'login',
+ fields: {
+ userEmail: 'tktmdals@naver.com',
+ password: '1234',
+ },
};
it('renders login form text', () => {
const { container, getByPlaceholderText } = renderAuthForm(login);
- expect(container).toHaveTextContent('Login');
+ expect(container).toHaveTextContent('로그인');
expect(getByPlaceholderText('이메일')).not.toBeNull();
expect(getByPlaceholderText('비밀번호')).not.toBeNull();
});
+
+ it('listens event call change', () => {
+ const { getByPlaceholderText } = renderAuthForm(login);
+
+ const inputs = [
+ { value: 'seungmin@naver.com', name: 'userEmail', placeholder: '이메일' },
+ { value: '345', name: 'password', placeholder: '비밀번호' },
+ ];
+
+ inputs.forEach(({ name, value, placeholder }) => {
+ const field = getByPlaceholderText(placeholder);
+
+ expect(field).not.toBeNull();
+
+ fireEvent.change(field, { target: { value, name } });
+
+ expect(handleChange).toBeCalled();
+ });
+ });
});
context('when type is register', () => {
const register = {
- type: 'Register',
+ type: 'register',
+ fields: {
+ userEmail: 'tktmdals@naver.com',
+ password: '1234',
+ passwordConfirm: '1234',
+ },
};
it('renders register form text', () => {
const { container, getByPlaceholderText } = renderAuthForm(register);
- expect(container).toHaveTextContent('Register');
+ expect(container).toHaveTextContent('회원가입');
expect(getByPlaceholderText('이메일')).not.toBeNull();
expect(getByPlaceholderText('비밀번호')).not.toBeNull();
expect(getByPlaceholderText('비밀번호 확인')).not.toBeNull();
});
+
+ it('listens event call change', () => {
+ const { getByPlaceholderText } = renderAuthForm(register);
+
+ const inputs = [
+ { value: 'seungmin@naver.com', name: 'userEmail', placeholder: '이메일' },
+ { value: '345', name: 'password', placeholder: '비밀번호' },
+ { value: '345', name: 'passwordConfirm', placeholder: '비밀번호 확인' },
+ ];
+
+ inputs.forEach(({ name, value, placeholder }) => {
+ const field = getByPlaceholderText(placeholder);
+
+ expect(field).not.toBeNull();
+
+ fireEvent.change(field, { target: { value, name } });
+
+ expect(handleChange).toBeCalled();
+ });
+ });
});
});
diff --git a/src/containers/auth/LoginFormContainer.jsx b/src/containers/auth/LoginFormContainer.jsx
new file mode 100644
index 0000000..3d450e0
--- /dev/null
+++ b/src/containers/auth/LoginFormContainer.jsx
@@ -0,0 +1,34 @@
+import React, { useCallback } from 'react';
+
+import { useSelector, useDispatch } from 'react-redux';
+
+import { get } from '../../util/utils';
+import { changeAuthField } from '../../reducers/slice';
+
+import AuthForm from '../../components/auth/AuthForm';
+
+const LoginFormContainer = () => {
+ const dispatch = useDispatch();
+
+ const login = useSelector(get('login'));
+
+ const onChangeLoginField = useCallback(({ name, value }) => {
+ dispatch(
+ changeAuthField({
+ form: 'login',
+ name,
+ value,
+ }),
+ );
+ });
+
+ return (
+
+ );
+};
+
+export default LoginFormContainer;
diff --git a/src/containers/auth/LoginFormContainer.test.jsx b/src/containers/auth/LoginFormContainer.test.jsx
new file mode 100644
index 0000000..08a3495
--- /dev/null
+++ b/src/containers/auth/LoginFormContainer.test.jsx
@@ -0,0 +1,63 @@
+import React from 'react';
+
+import { useDispatch, useSelector } from 'react-redux';
+
+import { render, fireEvent } from '@testing-library/react';
+
+import LoginFormContainer from './LoginFormContainer';
+
+describe('LoginFormContainer', () => {
+ const dispatch = jest.fn();
+
+ beforeEach(() => {
+ dispatch.mockClear();
+ useDispatch.mockImplementation(() => dispatch);
+
+ useSelector.mockImplementation((selector) => selector({
+ login: {
+ userEmail: '',
+ password: '',
+ },
+ }));
+ });
+
+ const renderLoginFormContainer = () => render((
+
+ ));
+
+ it('renders login form text', () => {
+ const { container, getByPlaceholderText } = renderLoginFormContainer();
+
+ expect(container).toHaveTextContent('로그인');
+ expect(getByPlaceholderText('이메일')).not.toBeNull();
+ expect(getByPlaceholderText('비밀번호')).not.toBeNull();
+ });
+
+ describe('action dispatch in login page', () => {
+ it('change event calls dispatch', () => {
+ const { getByPlaceholderText } = renderLoginFormContainer();
+
+ const inputs = [
+ { value: 'seungmin@naver.com', name: 'userEmail', placeholder: '이메일' },
+ { value: '345', name: 'password', placeholder: '비밀번호' },
+ ];
+
+ inputs.forEach(({ name, value, placeholder }) => {
+ const field = getByPlaceholderText(placeholder);
+
+ expect(field).not.toBeNull();
+
+ fireEvent.change(field, { target: { value, name } });
+
+ expect(dispatch).toBeCalledWith({
+ type: 'application/changeAuthField',
+ payload: {
+ form: 'login',
+ name,
+ value,
+ },
+ });
+ });
+ });
+ });
+});
diff --git a/src/containers/auth/RegisterFormContainer.jsx b/src/containers/auth/RegisterFormContainer.jsx
new file mode 100644
index 0000000..ecdbc9c
--- /dev/null
+++ b/src/containers/auth/RegisterFormContainer.jsx
@@ -0,0 +1,34 @@
+import React, { useCallback } from 'react';
+
+import { useSelector, useDispatch } from 'react-redux';
+
+import { get } from '../../util/utils';
+import { changeAuthField } from '../../reducers/slice';
+
+import AuthForm from '../../components/auth/AuthForm';
+
+const RegisterFormContainer = () => {
+ const dispatch = useDispatch();
+
+ const register = useSelector(get('register'));
+
+ const onChangeRegisterField = useCallback(({ name, value }) => {
+ dispatch(
+ changeAuthField({
+ form: 'register',
+ name,
+ value,
+ }),
+ );
+ });
+
+ return (
+
+ );
+};
+
+export default RegisterFormContainer;
diff --git a/src/containers/auth/RegisterFormContainer.test.jsx b/src/containers/auth/RegisterFormContainer.test.jsx
new file mode 100644
index 0000000..09bfc35
--- /dev/null
+++ b/src/containers/auth/RegisterFormContainer.test.jsx
@@ -0,0 +1,66 @@
+import React from 'react';
+
+import { useDispatch, useSelector } from 'react-redux';
+
+import { render, fireEvent } from '@testing-library/react';
+
+import RegisterFormContainer from './RegisterFormContainer';
+
+describe('RegisterFormContainer', () => {
+ const dispatch = jest.fn();
+
+ beforeEach(() => {
+ dispatch.mockClear();
+ useDispatch.mockImplementation(() => dispatch);
+
+ useSelector.mockImplementation((selector) => selector({
+ register: {
+ userEmail: '',
+ password: '',
+ passwordConfirm: '',
+ },
+ }));
+ });
+
+ const renderRegisterFormContainer = () => render((
+
+ ));
+
+ it('renders register form text', () => {
+ const { container, getByPlaceholderText } = renderRegisterFormContainer();
+
+ expect(container).toHaveTextContent('회원가입');
+ expect(getByPlaceholderText('이메일')).not.toBeNull();
+ expect(getByPlaceholderText('비밀번호')).not.toBeNull();
+ expect(getByPlaceholderText('비밀번호 확인')).not.toBeNull();
+ });
+
+ describe('action dispatch in register page', () => {
+ it('change event calls dispatch', () => {
+ const { getByPlaceholderText } = renderRegisterFormContainer();
+
+ const inputs = [
+ { value: 'seungmin@naver.com', name: 'userEmail', placeholder: '이메일' },
+ { value: '345', name: 'password', placeholder: '비밀번호' },
+ { value: '345', name: 'passwordConfirm', placeholder: '비밀번호 확인' },
+ ];
+
+ inputs.forEach(({ name, value, placeholder }) => {
+ const field = getByPlaceholderText(placeholder);
+
+ expect(field).not.toBeNull();
+
+ fireEvent.change(field, { target: { value, name } });
+
+ expect(dispatch).toBeCalledWith({
+ type: 'application/changeAuthField',
+ payload: {
+ form: 'register',
+ name,
+ value,
+ },
+ });
+ });
+ });
+ });
+});
diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx
index 2e55beb..913f426 100644
--- a/src/pages/LoginPage.jsx
+++ b/src/pages/LoginPage.jsx
@@ -1,9 +1,9 @@
import React from 'react';
-import AuthForm from '../components/auth/AuthForm';
+import LoginFormContainer from '../containers/auth/LoginFormContainer';
const LoginPage = () => (
-
+
);
export default LoginPage;
diff --git a/src/pages/LoginPage.test.jsx b/src/pages/LoginPage.test.jsx
index a5de1f8..17e4f76 100644
--- a/src/pages/LoginPage.test.jsx
+++ b/src/pages/LoginPage.test.jsx
@@ -1,10 +1,27 @@
import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+
import { render } from '@testing-library/react';
import LoginPage from './LoginPage';
describe('LoginPage', () => {
+ const dispatch = jest.fn();
+
+ beforeEach(() => {
+ dispatch.mockClear();
+
+ useDispatch.mockImplementation(() => dispatch);
+
+ useSelector.mockImplementation((selector) => selector({
+ login: {
+ userEmail: '',
+ password: '',
+ },
+ }));
+ });
+
const renderLoginPage = () => render((
));
@@ -13,7 +30,7 @@ describe('LoginPage', () => {
it('renders Login page Title', () => {
const { container } = renderLoginPage();
- expect(container).toHaveTextContent('Login');
+ expect(container).toHaveTextContent('로그인');
});
});
});
diff --git a/src/pages/RegisterPage.jsx b/src/pages/RegisterPage.jsx
index ab98c1b..e2a582a 100644
--- a/src/pages/RegisterPage.jsx
+++ b/src/pages/RegisterPage.jsx
@@ -1,9 +1,9 @@
import React from 'react';
-import AuthForm from '../components/auth/AuthForm';
+import RegisterFormContainer from '../containers/auth/RegisterFormContainer';
const RegisterPage = () => (
-
+
);
export default RegisterPage;
diff --git a/src/pages/RegisterPage.test.jsx b/src/pages/RegisterPage.test.jsx
index 98cb5b2..ecd6e85 100644
--- a/src/pages/RegisterPage.test.jsx
+++ b/src/pages/RegisterPage.test.jsx
@@ -1,10 +1,28 @@
import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+
import { render } from '@testing-library/react';
import RegisterPage from './RegisterPage';
describe('RegisterPage', () => {
+ const dispatch = jest.fn();
+
+ beforeEach(() => {
+ dispatch.mockClear();
+
+ useDispatch.mockImplementation(() => dispatch);
+
+ useSelector.mockImplementation((selector) => selector({
+ register: {
+ userEmail: '',
+ password: '',
+ passwordConfirm: '',
+ },
+ }));
+ });
+
const renderRegisterPage = () => render((
));
@@ -13,7 +31,7 @@ describe('RegisterPage', () => {
it('renders Register page Title', () => {
const { container } = renderRegisterPage();
- expect(container).toHaveTextContent('Register');
+ expect(container).toHaveTextContent('회원가입');
});
});
});
diff --git a/src/reducers/slice.js b/src/reducers/slice.js
index 9c66090..dcb2ae4 100644
--- a/src/reducers/slice.js
+++ b/src/reducers/slice.js
@@ -1,5 +1,7 @@
import { createSlice } from '@reduxjs/toolkit';
+import produce from 'immer';
+
import {
getStudyGroup,
getStudyGroups,
@@ -16,6 +18,18 @@ const writeInitialState = {
tags: [],
};
+const authInitialState = {
+ register: {
+ userEmail: '',
+ password: '',
+ passwordConfirm: '',
+ },
+ login: {
+ userEmail: '',
+ password: '',
+ },
+};
+
const { actions, reducer } = createSlice({
name: 'application',
initialState: {
@@ -23,7 +37,10 @@ const { actions, reducer } = createSlice({
group: null,
groupId: null,
writeField: writeInitialState,
+ register: authInitialState.register,
+ login: authInitialState.login,
},
+
reducers: {
setStudyGroups(state, { payload: { groups, tag } }) {
return {
@@ -37,12 +54,14 @@ const { actions, reducer } = createSlice({
}, []) : groups,
};
},
+
setStudyGroup(state, { payload: group }) {
return {
...state,
group,
};
},
+
changeWriteField(state, { payload: { name, value } }) {
const { writeField } = state;
@@ -54,6 +73,7 @@ const { actions, reducer } = createSlice({
},
};
},
+
clearWriteFields(state) {
return {
...state,
@@ -62,12 +82,19 @@ const { actions, reducer } = createSlice({
},
};
},
+
successWrite(state, { payload: groupId }) {
return {
...state,
groupId,
};
},
+
+ changeAuthField(state, { payload: { form, name, value } }) {
+ return produce(state, (draft) => {
+ draft[form][name] = value;
+ });
+ },
},
});
@@ -77,6 +104,7 @@ export const {
changeWriteField,
clearWriteFields,
successWrite,
+ changeAuthField,
} = actions;
export const loadStudyGroups = (tag) => async (dispatch) => {
diff --git a/src/reducers/slice.test.js b/src/reducers/slice.test.js
index 539dd84..158aa62 100644
--- a/src/reducers/slice.test.js
+++ b/src/reducers/slice.test.js
@@ -12,6 +12,7 @@ import reducer, {
writeStudyGroup,
clearWriteFields,
successWrite,
+ changeAuthField,
} from './slice';
import STUDY_GROUPS from '../../fixtures/study-groups';
@@ -38,6 +39,15 @@ describe('reducer', () => {
personnel: 0,
tags: [],
},
+ register: {
+ userEmail: '',
+ password: '',
+ passwordConfirm: '',
+ },
+ login: {
+ userEmail: '',
+ password: '',
+ },
};
it('returns initialState', () => {
@@ -143,6 +153,50 @@ describe('reducer', () => {
expect(state.groupId).toBe('1');
});
});
+
+ describe('changeAuthField', () => {
+ const initialState = {
+ register: {
+ userEmail: '',
+ password: '',
+ passwordConfirm: '',
+ },
+ login: {
+ userEmail: '',
+ password: '',
+ },
+ };
+
+ context('When the form name is login', () => {
+ it('login form is change', () => {
+ const state = reducer(initialState,
+ changeAuthField(
+ {
+ form: 'login',
+ name: 'userEmail',
+ value: 'tktmdals',
+ },
+ ));
+
+ expect(state.login.userEmail).toBe('tktmdals');
+ });
+ });
+
+ context('When the form name is register', () => {
+ it('register form is change', () => {
+ const state = reducer(initialState,
+ changeAuthField(
+ {
+ form: 'register',
+ name: 'userEmail',
+ value: 'tktmdals',
+ },
+ ));
+
+ expect(state.register.userEmail).toBe('tktmdals');
+ });
+ });
+ });
});
describe('async actions', () => {