diff --git a/src/App.jsx b/src/App.jsx index a476693..559be27 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,7 +4,7 @@ import { useDispatch } from 'react-redux'; import { Switch, Route } from 'react-router-dom'; import { loadItem } from './services/storage'; -import { setUser } from './reducers/slice'; +import { setUser } from './reducers/authSlice'; import MainPage from './pages/MainPage'; import WritePage from './pages/WritePage'; diff --git a/src/App.test.jsx b/src/App.test.jsx index 33b2479..00f7daa 100644 --- a/src/App.test.jsx +++ b/src/App.test.jsx @@ -24,19 +24,23 @@ describe('App', () => { useDispatch.mockImplementation(() => dispatch); useSelector.mockImplementation((selector) => selector({ - groups: STUDY_GROUPS, - group: given.group, - writeField: { - tags: [], + groupReducer: { + groups: STUDY_GROUPS, + group: given.group, + writeField: { + tags: [], + }, }, - register: { - userEmail: '', - password: '', - passwordConfirm: '', - }, - login: { - userEmail: '', - password: '', + authReducer: { + register: { + userEmail: '', + password: '', + passwordConfirm: '', + }, + login: { + userEmail: '', + password: '', + }, }, })); }); @@ -103,7 +107,7 @@ describe('App', () => { renderApp({ path: '/' }); expect(dispatch).toBeCalledWith({ - type: 'application/setUser', + type: 'auth/setUser', payload: user.email, }); }); diff --git a/src/containers/auth/LoginFormContainer.jsx b/src/containers/auth/LoginFormContainer.jsx index 4ebcb79..3bd96d0 100644 --- a/src/containers/auth/LoginFormContainer.jsx +++ b/src/containers/auth/LoginFormContainer.jsx @@ -4,10 +4,10 @@ import { useUnmount } from 'react-use'; import { useHistory } from 'react-router-dom'; import { useSelector, useDispatch } from 'react-redux'; -import { get, isCheckValidate } from '../../util/utils'; +import { getAuth, isCheckValidate } from '../../util/utils'; import { changeAuthField, clearAuth, clearAuthFields, requestLogin, -} from '../../reducers/slice'; +} from '../../reducers/authSlice'; import { ERROR_MESSAGE, FIREBASE_AUTH_ERROR_MESSAGE } from '../../util/messages'; import AuthForm from '../../components/auth/AuthForm'; @@ -20,9 +20,9 @@ const LoginFormContainer = () => { const dispatch = useDispatch(); const history = useHistory(); - const login = useSelector(get('login')); - const user = useSelector(get('user')); - const authError = useSelector(get('authError')); + const login = useSelector(getAuth('login')); + const user = useSelector(getAuth('user')); + const authError = useSelector(getAuth('authError')); const onChangeLoginField = useCallback(({ name, value }) => { dispatch( diff --git a/src/containers/auth/LoginFormContainer.test.jsx b/src/containers/auth/LoginFormContainer.test.jsx index 717a30f..e15c223 100644 --- a/src/containers/auth/LoginFormContainer.test.jsx +++ b/src/containers/auth/LoginFormContainer.test.jsx @@ -25,9 +25,11 @@ describe('LoginFormContainer', () => { useDispatch.mockImplementation(() => dispatch); useSelector.mockImplementation((selector) => selector({ - login: given.login, - user: given.user, - authError: given.authError, + authReducer: { + login: given.login, + user: given.user, + authError: given.authError, + }, })); }); @@ -70,7 +72,7 @@ describe('LoginFormContainer', () => { fireEvent.change(field, { target: { value, name } }); expect(dispatch).toBeCalledWith({ - type: 'application/changeAuthField', + type: 'auth/changeAuthField', payload: { form: 'login', name, @@ -149,7 +151,7 @@ describe('LoginFormContainer', () => { expect(container).toHaveTextContent('로그인에 실패하였습니다.'); expect(dispatch).toBeCalledWith({ - type: 'application/clearAuthFields', + type: 'auth/clearAuthFields', }); }); }); diff --git a/src/containers/auth/RegisterFormContainer.jsx b/src/containers/auth/RegisterFormContainer.jsx index 788a00c..f71ab5c 100644 --- a/src/containers/auth/RegisterFormContainer.jsx +++ b/src/containers/auth/RegisterFormContainer.jsx @@ -4,10 +4,10 @@ import { useUnmount } from 'react-use'; import { useHistory } from 'react-router-dom'; import { useSelector, useDispatch } from 'react-redux'; -import { get, isCheckValidate } from '../../util/utils'; +import { getAuth, isCheckValidate } from '../../util/utils'; import { changeAuthField, clearAuth, clearAuthFields, requestRegister, -} from '../../reducers/slice'; +} from '../../reducers/authSlice'; import { ERROR_MESSAGE, FIREBASE_AUTH_ERROR_MESSAGE } from '../../util/messages'; import AuthForm from '../../components/auth/AuthForm'; @@ -20,10 +20,10 @@ const RegisterFormContainer = () => { const dispatch = useDispatch(); const history = useHistory(); - const register = useSelector(get('register')); - const auth = useSelector(get('auth')); - const user = useSelector(get('user')); - const authError = useSelector(get('authError')); + const register = useSelector(getAuth('register')); + const auth = useSelector(getAuth('auth')); + const user = useSelector(getAuth('user')); + const authError = useSelector(getAuth('authError')); const onChangeRegisterField = useCallback(({ name, value }) => { dispatch( diff --git a/src/containers/auth/RegisterFormContainer.test.jsx b/src/containers/auth/RegisterFormContainer.test.jsx index 8f7c588..5c054fd 100644 --- a/src/containers/auth/RegisterFormContainer.test.jsx +++ b/src/containers/auth/RegisterFormContainer.test.jsx @@ -26,10 +26,12 @@ describe('RegisterFormContainer', () => { useDispatch.mockImplementation(() => dispatch); useSelector.mockImplementation((selector) => selector({ - user: given.user, - auth: given.auth, - authError: given.authError, - register: given.register, + authReducer: { + user: given.user, + auth: given.auth, + authError: given.authError, + register: given.register, + }, })); }); @@ -78,7 +80,7 @@ describe('RegisterFormContainer', () => { fireEvent.change(field, { target: { value, name } }); expect(dispatch).toBeCalledWith({ - type: 'application/changeAuthField', + type: 'auth/changeAuthField', payload: { form: 'register', name, @@ -153,7 +155,7 @@ describe('RegisterFormContainer', () => { name: 'password', value: '', }, - type: 'application/changeAuthField', + type: 'auth/changeAuthField', }); expect(container).toHaveTextContent('비밀번호가 일치하지 않습니다.'); diff --git a/src/containers/common/HeaderContainer.jsx b/src/containers/common/HeaderContainer.jsx index e9a8e31..bd23834 100644 --- a/src/containers/common/HeaderContainer.jsx +++ b/src/containers/common/HeaderContainer.jsx @@ -3,8 +3,8 @@ import React, { useCallback } from 'react'; import { useHistory } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; -import { get } from '../../util/utils'; -import { requestLogout } from '../../reducers/slice'; +import { getAuth } from '../../util/utils'; +import { requestLogout } from '../../reducers/authSlice'; import Header from '../../components/common/Header'; @@ -12,7 +12,7 @@ const HeaderContainer = () => { const dispatch = useDispatch(); const history = useHistory(); - const user = useSelector(get('user')); + const user = useSelector(getAuth('user')); const onLogout = useCallback(() => { dispatch(requestLogout()); diff --git a/src/containers/common/HeaderContainer.test.jsx b/src/containers/common/HeaderContainer.test.jsx index dba5880..624b2b3 100644 --- a/src/containers/common/HeaderContainer.test.jsx +++ b/src/containers/common/HeaderContainer.test.jsx @@ -26,7 +26,9 @@ describe('HeaderContainer', () => { useDispatch.mockImplementation(() => dispatch); useSelector.mockImplementation((selector) => selector({ - user: given.user, + authReducer: { + user: given.user, + }, })); }); diff --git a/src/containers/groups/StudyGroupsContainer.jsx b/src/containers/groups/StudyGroupsContainer.jsx index 2016417..7a60a2d 100644 --- a/src/containers/groups/StudyGroupsContainer.jsx +++ b/src/containers/groups/StudyGroupsContainer.jsx @@ -6,8 +6,8 @@ import { useSelector, useDispatch } from 'react-redux'; import qs from 'qs'; -import { get } from '../../util/utils'; -import { loadStudyGroups } from '../../reducers/slice'; +import { getAuth, getGroup } from '../../util/utils'; +import { loadStudyGroups } from '../../reducers/groupSlice'; import StudyGroups from '../../components/main/StudyGroups'; @@ -17,8 +17,8 @@ const StudyGroupsContainer = () => { const dispatch = useDispatch(); - const groups = useSelector(get('groups')); - const user = useSelector(get('user')); + const groups = useSelector(getGroup('groups')); + const user = useSelector(getAuth('user')); useInterval(() => { setRealTime(Date.now()); diff --git a/src/containers/groups/StudyGroupsContainer.test.jsx b/src/containers/groups/StudyGroupsContainer.test.jsx index 88af13e..f4bfe9b 100644 --- a/src/containers/groups/StudyGroupsContainer.test.jsx +++ b/src/containers/groups/StudyGroupsContainer.test.jsx @@ -15,7 +15,12 @@ describe('StudyGroupsContainer', () => { useDispatch.mockImplementation(() => dispatch); useSelector.mockImplementation((selector) => selector({ - groups: given.groups, + groupReducer: { + groups: given.groups, + }, + authReducer: { + user: 'user1', + }, })); }); diff --git a/src/containers/introduce/IntroduceContainer.jsx b/src/containers/introduce/IntroduceContainer.jsx index e172017..14cee81 100644 --- a/src/containers/introduce/IntroduceContainer.jsx +++ b/src/containers/introduce/IntroduceContainer.jsx @@ -3,8 +3,8 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useInterval } from 'react-use'; import { useDispatch, useSelector } from 'react-redux'; -import { get } from '../../util/utils'; -import { loadStudyGroup, updateStudyGroup } from '../../reducers/slice'; +import { getAuth, getGroup } from '../../util/utils'; +import { loadStudyGroup, updateStudyGroup } from '../../reducers/groupSlice'; import StudyIntroduceForm from '../../components/introduce/StudyIntroduceForm'; @@ -13,8 +13,8 @@ const IntroduceContainer = ({ groupId }) => { const dispatch = useDispatch(); - const group = useSelector(get('group')); - const user = useSelector(get('user')); + const group = useSelector(getGroup('group')); + const user = useSelector(getAuth('user')); useEffect(() => { dispatch(loadStudyGroup(groupId)); diff --git a/src/containers/introduce/IntroduceContainer.test.jsx b/src/containers/introduce/IntroduceContainer.test.jsx index cf29ff5..a0dbc45 100644 --- a/src/containers/introduce/IntroduceContainer.test.jsx +++ b/src/containers/introduce/IntroduceContainer.test.jsx @@ -19,8 +19,12 @@ describe('IntroduceContainer', () => { useDispatch.mockImplementation(() => dispatch); useSelector.mockImplementation((state) => state({ - group: given.group, - user: given.user, + authReducer: { + user: given.user, + }, + groupReducer: { + group: given.group, + }, })); }); diff --git a/src/containers/write/TagsFormContainer.jsx b/src/containers/write/TagsFormContainer.jsx index 6bc5077..711fe69 100644 --- a/src/containers/write/TagsFormContainer.jsx +++ b/src/containers/write/TagsFormContainer.jsx @@ -2,15 +2,15 @@ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { get } from '../../util/utils'; +import { getGroup } from '../../util/utils'; import TagsForm from '../../components/write/TagsForm'; -import { changeWriteField } from '../../reducers/slice'; +import { changeWriteField } from '../../reducers/groupSlice'; const TagsFormContainer = () => { const dispatch = useDispatch(); - const { tags } = useSelector(get('writeField')); + const { tags } = useSelector(getGroup('writeField')); const onChangeTags = (nextTags) => { dispatch( diff --git a/src/containers/write/TagsFormContainer.test.jsx b/src/containers/write/TagsFormContainer.test.jsx index ead8238..38ee916 100644 --- a/src/containers/write/TagsFormContainer.test.jsx +++ b/src/containers/write/TagsFormContainer.test.jsx @@ -15,8 +15,10 @@ describe('TagsFormContainer', () => { useDispatch.mockImplementation(() => dispatch); useSelector.mockImplementation((state) => state({ - writeField: { - tags: [], + groupReducer: { + writeField: { + tags: [], + }, }, })); }); @@ -49,7 +51,7 @@ describe('TagsFormContainer', () => { expect(input).toHaveValue(''); }); expect(dispatch).toBeCalledWith({ - type: 'application/changeWriteField', + type: 'group/changeWriteField', payload: { name: 'tags', value: tags }, }); }); diff --git a/src/containers/write/WriteButtonsContainer.jsx b/src/containers/write/WriteButtonsContainer.jsx index f0feef2..593e542 100644 --- a/src/containers/write/WriteButtonsContainer.jsx +++ b/src/containers/write/WriteButtonsContainer.jsx @@ -4,8 +4,8 @@ import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { ERROR_MESSAGE } from '../../util/messages'; -import { get, isCheckValidate } from '../../util/utils'; -import { writeStudyGroup } from '../../reducers/slice'; +import { getAuth, getGroup, isCheckValidate } from '../../util/utils'; +import { writeStudyGroup } from '../../reducers/groupSlice'; import WriteButtons from '../../components/write/WriteButtons'; @@ -21,9 +21,9 @@ const WriteButtonsContainer = () => { const history = useHistory(); const dispatch = useDispatch(); - const writeField = useSelector(get('writeField')); - const groupId = useSelector(get('groupId')); - const user = useSelector(get('user')); + const writeField = useSelector(getGroup('writeField')); + const groupId = useSelector(getGroup('groupId')); + const user = useSelector(getAuth('user')); const { title, applyEndDate, personnel, tags, diff --git a/src/containers/write/WriteButtonsContainer.test.jsx b/src/containers/write/WriteButtonsContainer.test.jsx index 3b1bb1a..ff72bbd 100644 --- a/src/containers/write/WriteButtonsContainer.test.jsx +++ b/src/containers/write/WriteButtonsContainer.test.jsx @@ -28,9 +28,13 @@ describe('WriteButtonsContainer', () => { useDispatch.mockImplementation(() => dispatch); useSelector.mockImplementation((state) => state({ - writeField: given.writeField, - groupId: given.groupId, - user: given.user, + groupReducer: { + writeField: given.writeField, + groupId: given.groupId, + }, + authReducer: { + user: given.user, + }, })); }); diff --git a/src/containers/write/WriteEditorContainer.jsx b/src/containers/write/WriteEditorContainer.jsx index 1773bbd..0f77ef6 100644 --- a/src/containers/write/WriteEditorContainer.jsx +++ b/src/containers/write/WriteEditorContainer.jsx @@ -2,7 +2,7 @@ import React, { useCallback } from 'react'; import { useDispatch } from 'react-redux'; -import { changeWriteField } from '../../reducers/slice'; +import { changeWriteField } from '../../reducers/groupSlice'; import WriteEditor from '../../components/write/WriteEditor'; diff --git a/src/containers/write/WriteEditorContainer.test.jsx b/src/containers/write/WriteEditorContainer.test.jsx index 15ba2b6..b0a489e 100644 --- a/src/containers/write/WriteEditorContainer.test.jsx +++ b/src/containers/write/WriteEditorContainer.test.jsx @@ -15,8 +15,10 @@ describe('WriteEditorContainer', () => { useDispatch.mockImplementation(() => dispatch); useSelector.mockImplementation((state) => state({ - writeField: { - contents: '', + groupReducer: { + writeField: { + contents: '', + }, }, })); }); diff --git a/src/containers/write/WriteFormContainer.jsx b/src/containers/write/WriteFormContainer.jsx index 5d2d2f2..06d6d21 100644 --- a/src/containers/write/WriteFormContainer.jsx +++ b/src/containers/write/WriteFormContainer.jsx @@ -3,15 +3,15 @@ import React from 'react'; import { useUnmount } from 'react-use'; import { useDispatch, useSelector } from 'react-redux'; -import { get } from '../../util/utils'; +import { getGroup } from '../../util/utils'; import WriteForm from '../../components/write/WriteForm'; -import { changeWriteField, clearWriteFields } from '../../reducers/slice'; +import { changeWriteField, clearWriteFields } from '../../reducers/groupSlice'; const WriteFormContainer = () => { const dispatch = useDispatch(); - const writeField = useSelector(get('writeField')); + const writeField = useSelector(getGroup('writeField')); const onChangeWriteField = ({ name, value }) => { dispatch( diff --git a/src/containers/write/WriteFormContainer.test.jsx b/src/containers/write/WriteFormContainer.test.jsx index b4fab04..121eee1 100644 --- a/src/containers/write/WriteFormContainer.test.jsx +++ b/src/containers/write/WriteFormContainer.test.jsx @@ -15,13 +15,15 @@ describe('WriteFormContainer', () => { useDispatch.mockImplementation(() => dispatch); useSelector.mockImplementation((state) => state({ - writeField: { - title: '', - contents: '', - moderatorId: '', - applyEndDate: '', - participants: [], - personnel: 0, + groupReducer: { + writeField: { + title: '', + contents: '', + moderatorId: '', + applyEndDate: '', + participants: [], + personnel: 0, + }, }, })); }); @@ -61,7 +63,7 @@ describe('WriteFormContainer', () => { name, value, }, - type: 'application/changeWriteField', + type: 'group/changeWriteField', }); }); }); diff --git a/src/pages/IntroducePage.jsx b/src/pages/IntroducePage.jsx index 690ecca..ae46df6 100644 --- a/src/pages/IntroducePage.jsx +++ b/src/pages/IntroducePage.jsx @@ -3,6 +3,7 @@ import React from 'react'; import { useParams } from 'react-router-dom'; import Responsive from '../styles/Responsive'; + import IntroduceContainer from '../containers/introduce/IntroduceContainer'; const IntroducePage = ({ params }) => { diff --git a/src/pages/IntroducePage.test.jsx b/src/pages/IntroducePage.test.jsx index 939e175..b4d3666 100644 --- a/src/pages/IntroducePage.test.jsx +++ b/src/pages/IntroducePage.test.jsx @@ -14,19 +14,22 @@ describe('IntroducePage', () => { useDispatch.mockImplementation(() => dispatch); useSelector.mockImplementation((state) => state({ - group: { - id: 1, - moderatorId: 'user1', - title: '스터디를 소개합니다. 1', - personnel: 7, - contents: '우리는 이것저것 합니다.1', - participants: [], - tags: [ - 'JavaScript', - 'React', - 'Algorithm', - ], + groupReducer: { + group: { + id: 1, + moderatorId: 'user1', + title: '스터디를 소개합니다. 1', + personnel: 7, + contents: '우리는 이것저것 합니다.1', + participants: [], + tags: [ + 'JavaScript', + 'React', + 'Algorithm', + ], + }, }, + authReducer: {}, })); }); diff --git a/src/pages/LoginPage.test.jsx b/src/pages/LoginPage.test.jsx index 17e4f76..34ad85b 100644 --- a/src/pages/LoginPage.test.jsx +++ b/src/pages/LoginPage.test.jsx @@ -15,9 +15,11 @@ describe('LoginPage', () => { useDispatch.mockImplementation(() => dispatch); useSelector.mockImplementation((selector) => selector({ - login: { - userEmail: '', - password: '', + authReducer: { + login: { + userEmail: '', + password: '', + }, }, })); }); diff --git a/src/pages/MainPage.test.jsx b/src/pages/MainPage.test.jsx index 0c1c9e1..9804142 100644 --- a/src/pages/MainPage.test.jsx +++ b/src/pages/MainPage.test.jsx @@ -18,8 +18,12 @@ describe('MainPage', () => { useDispatch.mockImplementation(() => dispatch); useSelector.mockImplementation((selector) => selector({ - groups: STUDY_GROUPS, - user: given.user, + groupReducer: { + groups: STUDY_GROUPS, + }, + authReducer: { + user: given.user, + }, })); }); diff --git a/src/pages/RegisterPage.test.jsx b/src/pages/RegisterPage.test.jsx index ecd6e85..5740f8f 100644 --- a/src/pages/RegisterPage.test.jsx +++ b/src/pages/RegisterPage.test.jsx @@ -15,10 +15,12 @@ describe('RegisterPage', () => { useDispatch.mockImplementation(() => dispatch); useSelector.mockImplementation((selector) => selector({ - register: { - userEmail: '', - password: '', - passwordConfirm: '', + authReducer: { + register: { + userEmail: '', + password: '', + passwordConfirm: '', + }, }, })); }); diff --git a/src/pages/WritePage.jsx b/src/pages/WritePage.jsx index 704836e..b305edb 100644 --- a/src/pages/WritePage.jsx +++ b/src/pages/WritePage.jsx @@ -1,12 +1,12 @@ import React from 'react'; +import Responsive from '../styles/Responsive'; + import TagFormContainer from '../containers/write/TagsFormContainer'; import WriteButtonsContainer from '../containers/write/WriteButtonsContainer'; import WriteEditorContainer from '../containers/write/WriteEditorContainer'; import WriteFormContainer from '../containers/write/WriteFormContainer'; -import Responsive from '../styles/Responsive'; - const IntroducePage = () => (

스터디 그룹 개설하기

diff --git a/src/pages/WritePage.test.jsx b/src/pages/WritePage.test.jsx index ffa2d2f..c3fb288 100644 --- a/src/pages/WritePage.test.jsx +++ b/src/pages/WritePage.test.jsx @@ -15,8 +15,13 @@ describe('WritePage', () => { useDispatch.mockImplementation(() => dispatch); useSelector.mockImplementation((state) => state({ - writeField: { - tags: [], + groupReducer: { + writeField: { + tags: [], + }, + }, + authReducer: { + user: null, }, })); }); diff --git a/src/reducers/authSlice.js b/src/reducers/authSlice.js new file mode 100644 index 0000000..b01cc80 --- /dev/null +++ b/src/reducers/authSlice.js @@ -0,0 +1,138 @@ +import { createSlice } from '@reduxjs/toolkit'; + +import produce from 'immer'; + +import { + postUserLogin, + postUserLogout, + postUserRegister, +} from '../services/api'; + +import { removeItem, saveItem } from '../services/storage'; + +const authInitialState = { + register: { + userEmail: '', + password: '', + passwordConfirm: '', + }, + login: { + userEmail: '', + password: '', + }, +}; + +const { login, register } = authInitialState; + +const { actions, reducer: authReducer } = createSlice({ + name: 'auth', + initialState: { + login, + register, + user: null, + auth: null, + authError: null, + }, + + reducers: { + changeAuthField(state, { payload: { form, name, value } }) { + return produce(state, (draft) => { + draft[form][name] = value; + }); + }, + + clearAuthFields(state) { + return { + ...state, + register, + login, + }; + }, + + setAuth(state, { payload: auth }) { + return { + ...state, + auth, + }; + }, + + setAuthError(state, { payload: error }) { + return { + ...state, + authError: error, + }; + }, + + clearAuth(state) { + return { + ...state, + auth: null, + authError: null, + }; + }, + + setUser(state, { payload: user }) { + return { + ...state, + user, + }; + }, + + logout(state) { + return { + ...state, + user: null, + }; + }, + }, +}); + +export const { + changeAuthField, + clearAuthFields, + setAuth, + setAuthError, + clearAuth, + setUser, + logout, +} = actions; + +export const requestRegister = () => async (dispatch, getState) => { + const { register: { userEmail, password } } = getState(); + + try { + const { user } = await postUserRegister({ userEmail, password }); + + dispatch(setAuth(user.email)); + } catch (error) { + dispatch(setAuthError(error.code)); + } +}; + +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)); + } catch (error) { + dispatch(setAuthError(error.code)); + } +}; + +export const requestLogout = () => async (dispatch) => { + await postUserLogout(); + + removeItem('user'); + + dispatch(logout()); +}; + +export default authReducer; diff --git a/src/reducers/slice.test.js b/src/reducers/authSlice.test.js similarity index 62% rename from src/reducers/slice.test.js rename to src/reducers/authSlice.test.js index 6b31c36..6aa0e7e 100644 --- a/src/reducers/slice.test.js +++ b/src/reducers/authSlice.test.js @@ -4,14 +4,6 @@ import thunk from 'redux-thunk'; import configureStore from 'redux-mock-store'; import reducer, { - setStudyGroups, - loadStudyGroups, - setStudyGroup, - loadStudyGroup, - changeWriteField, - writeStudyGroup, - clearWriteFields, - successWrite, changeAuthField, clearAuthFields, setAuth, @@ -22,12 +14,8 @@ import reducer, { setUser, logout, requestLogout, - updateStudyGroup, -} from './slice'; +} from './authSlice'; -import STUDY_GROUPS from '../../fixtures/study-groups'; -import STUDY_GROUP from '../../fixtures/study-group'; -import WRITE_FORM from '../../fixtures/write-form'; import { postUserLogin, postUserLogout, postUserRegister } from '../services/api'; const middlewares = [thunk]; @@ -38,30 +26,18 @@ jest.mock('../services/api'); describe('reducer', () => { context('when previous state is undefined', () => { const initialState = { - groups: [], - group: null, - groupId: null, - user: null, - auth: null, - authError: null, - writeField: { - title: '', - contents: '', - moderatorId: '', - applyEndDate: '', - participants: [], - personnel: '1', - tags: [], - }, - register: { + login: { userEmail: '', password: '', - passwordConfirm: '', }, - login: { + register: { userEmail: '', password: '', + passwordConfirm: '', }, + user: null, + auth: null, + authError: null, }; it('returns initialState', () => { @@ -71,86 +47,6 @@ describe('reducer', () => { }); }); - describe('setStudyGroups', () => { - it('get study groups list', () => { - const initialState = { - groups: [], - }; - - const state = reducer( - initialState, - setStudyGroups(STUDY_GROUPS), - ); - - expect(state.groups).toHaveLength(2); - }); - }); - - describe('setStudyGroup', () => { - it('get group detail contents', () => { - const initialState = { - group: null, - }; - - const state = reducer(initialState, setStudyGroup(STUDY_GROUP)); - - expect(state.group.id).toBe(1); - }); - }); - - describe('changeWriteField', () => { - it('changes a field of establish study group write', () => { - const initialState = { - writeField: { - title: '', - contents: '', - moderatorId: '', - applyEndDate: '', - participants: [], - personnel: '1', - tags: [], - }, - }; - - const state = reducer( - initialState, - changeWriteField({ name: 'tags', value: ['JavaScript', 'React'] }), - ); - - expect(state.writeField.tags).toEqual(['JavaScript', 'React']); - }); - }); - - describe('clearWriteFields', () => { - const initialState = { - writeField: { - title: '타이틀', - contents: '내용', - }, - }; - - it('clears fields of write', () => { - const state = reducer(initialState, clearWriteFields()); - - const { writeField: { title, contents } } = state; - - expect(title).toBe(''); - expect(contents).toBe(''); - }); - }); - - describe('successWrite', () => { - const initialState = { - groupId: null, - }; - - it('groupId return after successful writing', () => { - const state = reducer(initialState, successWrite('1')); - - expect(state.groupId).toBe('1'); - }); - }); - describe('changeAuthField', () => { const initialState = { register: { @@ -288,72 +184,6 @@ describe('reducer', () => { describe('async actions', () => { let store; - describe('loadStudyGroups', () => { - beforeEach(() => { - store = mockStore({}); - }); - - it('loads study group list', async () => { - await store.dispatch(loadStudyGroups()); - - const actions = store.getActions(); - - expect(actions[0]).toEqual(setStudyGroups([])); - }); - }); - - describe('loadStudyGroup', () => { - beforeEach(() => { - store = mockStore({}); - }); - - it('load study group detail', async () => { - await store.dispatch(loadStudyGroup(1)); - - const actions = store.getActions(); - - expect(actions[0]).toEqual(setStudyGroup(null)); - expect(actions[1]).toEqual(setStudyGroup({ id: 1 })); - }); - }); - - describe('writeStudyGroup', () => { - beforeEach(() => { - store = mockStore({ - writeField: WRITE_FORM, - }); - }); - - it('dispatches clearWriteFields and successWrite', async () => { - await store.dispatch(writeStudyGroup()); - - const actions = store.getActions(); - - expect(actions[0]).toEqual(successWrite(undefined)); - expect(actions[1]).toEqual(clearWriteFields(undefined)); - }); - }); - - describe('updateStudyGroup', () => { - beforeEach(() => { - store = mockStore({ - group: STUDY_GROUP, - user: 'example', - }); - }); - - it('dispatches setStudyGroup', async () => { - await store.dispatch(updateStudyGroup()); - - const actions = store.getActions(); - - expect(actions[0]).toEqual(setStudyGroup({ - ...STUDY_GROUP, - participants: [...STUDY_GROUP.participants, 'example'], - })); - }); - }); - describe('requestRegister', () => { const register = { userEmail: 'seungmin@naver.com', diff --git a/src/reducers/groupSlice.js b/src/reducers/groupSlice.js new file mode 100644 index 0000000..0bda901 --- /dev/null +++ b/src/reducers/groupSlice.js @@ -0,0 +1,124 @@ +import { createSlice } from '@reduxjs/toolkit'; + +import produce from 'immer'; + +import { + getStudyGroup, + getStudyGroups, + postStudyGroup, + updateParticipants, +} from '../services/api'; + +const writeInitialState = { + title: '', + contents: '', + moderatorId: '', + applyEndDate: '', + participants: [], + personnel: '1', + tags: [], +}; + +const { actions, reducer: groupSlice } = createSlice({ + name: 'group', + initialState: { + groups: [], + group: null, + groupId: null, + writeField: writeInitialState, + }, + + reducers: { + setStudyGroups(state, { payload: groups }) { + return { + ...state, + groups, + }; + }, + + setStudyGroup(state, { payload: group }) { + return { + ...state, + group, + }; + }, + + changeWriteField(state, { payload: { name, value } }) { + const { writeField } = state; + + return { + ...state, + writeField: { + ...writeField, + [name]: value, + }, + }; + }, + + clearWriteFields(state) { + return { + ...state, + writeField: writeInitialState, + groupId: null, + }; + }, + + successWrite(state, { payload: groupId }) { + return { + ...state, + groupId, + }; + }, + }, +}); + +export const { + setStudyGroups, + setStudyGroup, + changeWriteField, + clearWriteFields, + successWrite, +} = actions; + +export const loadStudyGroups = (tag) => async (dispatch) => { + const groups = await getStudyGroups(tag); + + dispatch(setStudyGroups(groups)); +}; + +export const loadStudyGroup = (id) => async (dispatch) => { + dispatch(setStudyGroup(null)); + + const group = await getStudyGroup(id); + + dispatch(setStudyGroup({ + ...group, + id, + })); +}; + +export const writeStudyGroup = () => async (dispatch, getState) => { + const { writeField, user } = getState(); + + const groupId = await postStudyGroup(produce(writeField, (draft) => { + draft.moderatorId = user; + draft.participants.push(user); + })); + + dispatch(successWrite(groupId)); + dispatch(clearWriteFields()); +}; + +export const updateStudyGroup = () => async (dispatch, getState) => { + const { group, user } = getState(); + + const newGroup = produce(group, (draft) => { + draft.participants.push(user); + }); + + await updateParticipants(newGroup); + + dispatch(setStudyGroup(newGroup)); +}; + +export default groupSlice; diff --git a/src/reducers/groupSlice.test.js b/src/reducers/groupSlice.test.js new file mode 100644 index 0000000..67e6668 --- /dev/null +++ b/src/reducers/groupSlice.test.js @@ -0,0 +1,200 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import thunk from 'redux-thunk'; + +import configureStore from 'redux-mock-store'; + +import reducer, { + setStudyGroups, + loadStudyGroups, + setStudyGroup, + loadStudyGroup, + changeWriteField, + writeStudyGroup, + clearWriteFields, + successWrite, + updateStudyGroup, +} from './groupSlice'; + +import STUDY_GROUPS from '../../fixtures/study-groups'; +import STUDY_GROUP from '../../fixtures/study-group'; +import WRITE_FORM from '../../fixtures/write-form'; + +const middlewares = [thunk]; +const mockStore = configureStore(middlewares); + +jest.mock('../services/api'); + +describe('reducer', () => { + context('when previous state is undefined', () => { + const initialState = { + groups: [], + group: null, + groupId: null, + writeField: { + title: '', + contents: '', + moderatorId: '', + applyEndDate: '', + participants: [], + personnel: '1', + tags: [], + }, + }; + + it('returns initialState', () => { + const state = reducer(undefined, { type: 'action' }); + + expect(state).toEqual(initialState); + }); + }); + + describe('setStudyGroups', () => { + it('get study groups list', () => { + const initialState = { + groups: [], + }; + + const state = reducer( + initialState, + setStudyGroups(STUDY_GROUPS), + ); + + expect(state.groups).toHaveLength(2); + }); + }); + + describe('setStudyGroup', () => { + it('get group detail contents', () => { + const initialState = { + group: null, + }; + + const state = reducer(initialState, setStudyGroup(STUDY_GROUP)); + + expect(state.group.id).toBe(1); + }); + }); + + describe('changeWriteField', () => { + it('changes a field of establish study group write', () => { + const initialState = { + writeField: { + title: '', + contents: '', + moderatorId: '', + applyEndDate: '', + participants: [], + personnel: '1', + tags: [], + }, + }; + + const state = reducer( + initialState, + changeWriteField({ name: 'tags', value: ['JavaScript', 'React'] }), + ); + + expect(state.writeField.tags).toEqual(['JavaScript', 'React']); + }); + }); + + describe('clearWriteFields', () => { + const initialState = { + writeField: { + title: '타이틀', + contents: '내용', + }, + }; + + it('clears fields of write', () => { + const state = reducer(initialState, clearWriteFields()); + + const { writeField: { title, contents } } = state; + + expect(title).toBe(''); + expect(contents).toBe(''); + }); + }); + + describe('successWrite', () => { + const initialState = { + groupId: null, + }; + + it('groupId return after successful writing', () => { + const state = reducer(initialState, successWrite('1')); + + expect(state.groupId).toBe('1'); + }); + }); +}); + +describe('async actions', () => { + let store; + + describe('loadStudyGroups', () => { + beforeEach(() => { + store = mockStore({}); + }); + + it('loads study group list', async () => { + await store.dispatch(loadStudyGroups()); + + const actions = store.getActions(); + + expect(actions[0]).toEqual(setStudyGroups([])); + }); + }); + + describe('loadStudyGroup', () => { + beforeEach(() => { + store = mockStore({}); + }); + + it('load study group detail', async () => { + await store.dispatch(loadStudyGroup(1)); + + const actions = store.getActions(); + + expect(actions[0]).toEqual(setStudyGroup(null)); + expect(actions[1]).toEqual(setStudyGroup({ id: 1 })); + }); + }); + + describe('writeStudyGroup', () => { + beforeEach(() => { + store = mockStore({ + writeField: WRITE_FORM, + }); + }); + + it('dispatches clearWriteFields and successWrite', async () => { + await store.dispatch(writeStudyGroup()); + + const actions = store.getActions(); + + expect(actions[0]).toEqual(successWrite(undefined)); + expect(actions[1]).toEqual(clearWriteFields(undefined)); + }); + }); + + describe('updateStudyGroup', () => { + beforeEach(() => { + store = mockStore({ + group: STUDY_GROUP, + user: 'example', + }); + }); + + it('dispatches setStudyGroup', async () => { + await store.dispatch(updateStudyGroup()); + + const actions = store.getActions(); + + expect(actions[0]).toEqual(setStudyGroup({ + ...STUDY_GROUP, + participants: [...STUDY_GROUP.participants, 'example'], + })); + }); + }); +}); diff --git a/src/reducers/rootSlice.js b/src/reducers/rootSlice.js new file mode 100644 index 0000000..1a5a2a6 --- /dev/null +++ b/src/reducers/rootSlice.js @@ -0,0 +1,10 @@ +import { combineReducers } from '@reduxjs/toolkit'; +import authReducer from './authSlice'; +import groupReducer from './groupSlice'; + +const rootReducer = combineReducers({ + authReducer, + groupReducer, +}); + +export default rootReducer; diff --git a/src/reducers/slice.js b/src/reducers/slice.js deleted file mode 100644 index d26b9ae..0000000 --- a/src/reducers/slice.js +++ /dev/null @@ -1,241 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit'; - -import produce from 'immer'; - -import { - getStudyGroup, - getStudyGroups, - postStudyGroup, - postUserLogin, - postUserLogout, - postUserRegister, - updateParticipants, -} from '../services/api'; -import { removeItem, saveItem } from '../services/storage'; - -const writeInitialState = { - title: '', - contents: '', - moderatorId: '', - applyEndDate: '', - participants: [], - personnel: '1', - tags: [], -}; - -const authInitialState = { - register: { - userEmail: '', - password: '', - passwordConfirm: '', - }, - login: { - userEmail: '', - password: '', - }, -}; - -const { actions, reducer } = createSlice({ - name: 'application', - initialState: { - groups: [], - group: null, - groupId: null, - user: null, - auth: null, - authError: null, - writeField: writeInitialState, - register: authInitialState.register, - login: authInitialState.login, - }, - - reducers: { - setStudyGroups(state, { payload: groups }) { - return { - ...state, - groups, - }; - }, - - setStudyGroup(state, { payload: group }) { - return { - ...state, - group, - }; - }, - - changeWriteField(state, { payload: { name, value } }) { - const { writeField } = state; - - return { - ...state, - writeField: { - ...writeField, - [name]: value, - }, - }; - }, - - clearWriteFields(state) { - return { - ...state, - writeField: writeInitialState, - groupId: null, - }; - }, - - successWrite(state, { payload: groupId }) { - return { - ...state, - groupId, - }; - }, - - changeAuthField(state, { payload: { form, name, value } }) { - return produce(state, (draft) => { - draft[form][name] = value; - }); - }, - - clearAuthFields(state) { - const { register, login } = authInitialState; - - return { - ...state, - register, - login, - }; - }, - - setAuth(state, { payload: auth }) { - return { - ...state, - auth, - }; - }, - - setAuthError(state, { payload: error }) { - return { - ...state, - authError: error, - }; - }, - - clearAuth(state) { - return { - ...state, - auth: null, - authError: null, - }; - }, - - setUser(state, { payload: user }) { - return { - ...state, - user, - }; - }, - logout(state) { - return { - ...state, - user: null, - }; - }, - }, -}); - -export const { - setStudyGroups, - setStudyGroup, - changeWriteField, - clearWriteFields, - successWrite, - changeAuthField, - clearAuthFields, - setAuth, - setAuthError, - clearAuth, - setUser, - logout, -} = actions; - -export const loadStudyGroups = (tag) => async (dispatch) => { - const groups = await getStudyGroups(tag); - - dispatch(setStudyGroups(groups)); -}; - -export const loadStudyGroup = (id) => async (dispatch) => { - dispatch(setStudyGroup(null)); - - const group = await getStudyGroup(id); - - dispatch(setStudyGroup({ - ...group, - id, - })); -}; - -export const writeStudyGroup = () => async (dispatch, getState) => { - const { writeField, user } = getState(); - - const groupId = await postStudyGroup(produce(writeField, (draft) => { - draft.moderatorId = user; - draft.participants.push(user); - })); - - dispatch(successWrite(groupId)); - dispatch(clearWriteFields()); -}; - -export const updateStudyGroup = () => async (dispatch, getState) => { - const { group, user } = getState(); - - const newGroup = produce(group, (draft) => { - draft.participants.push(user); - }); - - await updateParticipants(newGroup); - - dispatch(setStudyGroup(newGroup)); -}; - -export const requestRegister = () => async (dispatch, getState) => { - const { register: { userEmail, password } } = getState(); - - try { - const { user } = await postUserRegister({ userEmail, password }); - - dispatch(setAuth(user.email)); - } catch (error) { - dispatch(setAuthError(error.code)); - } -}; - -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)); - } catch (error) { - dispatch(setAuthError(error.code)); - } -}; - -export const requestLogout = () => async (dispatch) => { - await postUserLogout(); - - removeItem('user'); - - dispatch(logout()); -}; - -export default reducer; diff --git a/src/reducers/store.js b/src/reducers/store.js index 79f0aa9..99c8f77 100644 --- a/src/reducers/store.js +++ b/src/reducers/store.js @@ -2,8 +2,8 @@ import { configureStore } from '@reduxjs/toolkit'; import { composeWithDevTools } from 'redux-devtools-extension'; import { createLogger } from 'redux-logger'; -import reducer from './slice'; +import rootReducer from './rootSlice'; -const store = configureStore({ reducer }, composeWithDevTools(createLogger())); +const store = configureStore({ reducer: rootReducer }, composeWithDevTools(createLogger())); export default store; diff --git a/src/util/utils.js b/src/util/utils.js index 4425d23..ee680c3 100644 --- a/src/util/utils.js +++ b/src/util/utils.js @@ -1,6 +1,6 @@ -export function get(key) { - return (obj) => obj[key]; -} +export const getAuth = (key) => (obj) => obj.authReducer[key]; + +export const getGroup = (key) => (obj) => obj.groupReducer[key]; export function equal(key, value) { return (obj) => obj[key] === value; diff --git a/src/util/utils.test.js b/src/util/utils.test.js index 4610361..066de35 100644 --- a/src/util/utils.test.js +++ b/src/util/utils.test.js @@ -1,12 +1,28 @@ -import { get, equal } from './utils'; +import { getAuth, getGroup, equal } from './utils'; -test('get', () => { +test('getAuth', () => { const state = { - name: '홍길동', + authReducer: { + name: '홍길동', + }, + }; + + const f = getAuth('name'); + const g = getAuth('age'); + + expect(f(state)).toBe('홍길동'); + expect(g(state)).toBeUndefined(); +}); + +test('getGroup', () => { + const state = { + groupReducer: { + name: '홍길동', + }, }; - const f = get('name'); - const g = get('age'); + const f = getGroup('name'); + const g = getGroup('age'); expect(f(state)).toBe('홍길동'); expect(g(state)).toBeUndefined();