From 891b7132eaa2919a481f7a7a266ca82efde7a5f0 Mon Sep 17 00:00:00 2001 From: saseungmin Date: Tue, 8 Dec 2020 21:25:17 +0900 Subject: [PATCH 1/2] [Refactoring & Feature] delete for Participant - delete apply user, create redux and api --- .../introduce/IntroduceContainer.jsx | 4 +- .../introduce/IntroduceContainer.test.jsx | 2 +- src/reducers/groupSlice.js | 32 ++++++++++++--- src/reducers/groupSlice.test.js | 40 +++++++++++++++++-- src/services/__mocks__/api.js | 4 +- src/services/api.js | 14 +++++-- 6 files changed, 80 insertions(+), 16 deletions(-) diff --git a/src/containers/introduce/IntroduceContainer.jsx b/src/containers/introduce/IntroduceContainer.jsx index 62ef687..6d1c12a 100644 --- a/src/containers/introduce/IntroduceContainer.jsx +++ b/src/containers/introduce/IntroduceContainer.jsx @@ -4,7 +4,7 @@ import { useInterval } from 'react-use'; import { useDispatch, useSelector } from 'react-redux'; import { getAuth, getGroup } from '../../util/utils'; -import { loadStudyGroup, updateStudyGroup } from '../../reducers/groupSlice'; +import { loadStudyGroup, updateParticipant } from '../../reducers/groupSlice'; import StudyIntroduceForm from '../../components/introduce/StudyIntroduceForm'; import GroupContentLoader from '../../components/introduce/GroupsContentLoader'; @@ -27,7 +27,7 @@ const IntroduceContainer = ({ groupId }) => { }, 1000); const onApplyStudy = useCallback(() => { - dispatch(updateStudyGroup()); + dispatch(updateParticipant()); }, [dispatch]); if (!group) { diff --git a/src/containers/introduce/IntroduceContainer.test.jsx b/src/containers/introduce/IntroduceContainer.test.jsx index ed8b820..7e3bf95 100644 --- a/src/containers/introduce/IntroduceContainer.test.jsx +++ b/src/containers/introduce/IntroduceContainer.test.jsx @@ -77,7 +77,7 @@ describe('IntroduceContainer', () => { given('group', () => (STUDY_GROUP)); given('user', () => ('user')); - it('click event dispatches action call updateStudyGroup', () => { + it('click event dispatches action call updateParticipant', () => { const { getByText } = renderIntroduceContainer(1); const button = getByText('신청하기'); diff --git a/src/reducers/groupSlice.js b/src/reducers/groupSlice.js index e1ea2b7..7f8b84f 100644 --- a/src/reducers/groupSlice.js +++ b/src/reducers/groupSlice.js @@ -6,7 +6,8 @@ import { getStudyGroup, getStudyGroups, postStudyGroup, - updateParticipants, + updatePostParticipant, + deletePostParticipant, } from '../services/api'; const writeInitialState = { @@ -112,14 +113,33 @@ export const writeStudyGroup = () => async (dispatch, getState) => { dispatch(clearWriteFields()); }; -export const updateStudyGroup = () => async (dispatch, getState) => { - const { groupReducer, authReducer } = getState(); +export const updateParticipant = () => async (dispatch, getState) => { + const { groupReducer: { group }, authReducer: { user } } = getState(); - const newGroup = produce(groupReducer.group, (draft) => { - draft.participants.push(authReducer.user); + const newGroup = produce(group, (draft) => { + draft.participants.push(user); }); - await updateParticipants(newGroup); + await updatePostParticipant({ + user, + id: group.id, + }); + + dispatch(setStudyGroup(newGroup)); +}; + +export const deleteParticipant = () => async (dispatch, getState) => { + const { groupReducer: { group }, authReducer: { user } } = getState(); + + const newGroup = { + ...group, + participants: group.participants.filter((participant) => participant !== user), + }; + + await deletePostParticipant({ + user, + id: group.id, + }); dispatch(setStudyGroup(newGroup)); }; diff --git a/src/reducers/groupSlice.test.js b/src/reducers/groupSlice.test.js index 2a4c654..aa92267 100644 --- a/src/reducers/groupSlice.test.js +++ b/src/reducers/groupSlice.test.js @@ -12,7 +12,8 @@ import reducer, { writeStudyGroup, clearWriteFields, successWrite, - updateStudyGroup, + updateParticipant, + deleteParticipant, } from './groupSlice'; import STUDY_GROUPS from '../../fixtures/study-groups'; @@ -196,7 +197,7 @@ describe('async actions', () => { }); }); - describe('updateStudyGroup', () => { + describe('updateParticipant', () => { beforeEach(() => { store = mockStore({ groupReducer: { @@ -209,7 +210,7 @@ describe('async actions', () => { }); it('dispatches setStudyGroup', async () => { - await store.dispatch(updateStudyGroup()); + await store.dispatch(updateParticipant()); const actions = store.getActions(); @@ -219,4 +220,37 @@ describe('async actions', () => { })); }); }); + + describe('deleteParticipant', () => { + const group = { + id: 1, + participants: [ + 'user2', + 'example', + ], + }; + const user = 'example'; + + beforeEach(() => { + store = mockStore({ + groupReducer: { + group, + }, + authReducer: { + user, + }, + }); + }); + + it('dispatches setStudyGroup', async () => { + await store.dispatch(deleteParticipant()); + + const actions = store.getActions(); + + expect(actions[0]).toEqual(setStudyGroup({ + ...group, + participants: group.participants.filter((participant) => participant !== user), + })); + }); + }); }); diff --git a/src/services/__mocks__/api.js b/src/services/__mocks__/api.js index f1f1aa7..840f49e 100644 --- a/src/services/__mocks__/api.js +++ b/src/services/__mocks__/api.js @@ -10,4 +10,6 @@ export const postUserLogin = jest.fn(); export const postUserLogout = jest.fn(); -export const updateParticipants = jest.fn(); +export const updatePostParticipant = jest.fn(); + +export const deletePostParticipant = jest.fn(); diff --git a/src/services/api.js b/src/services/api.js index 328b949..9a2887a 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -63,12 +63,20 @@ export const postStudyGroup = async (group) => { return id; }; -export const updateParticipants = async (group) => { - const { id, participants } = group; +export const updatePostParticipant = async ({ id, user }) => { + const groups = db.collection('groups').doc(id); + await groups.update({ + participants: fireStore.FieldValue.arrayUnion(user), + }); +}; + +export const deletePostParticipant = async ({ id, user }) => { const groups = db.collection('groups').doc(id); - await groups.update({ participants }); + await groups.update({ + participants: fireStore.FieldValue.arrayRemove(user), + }); }; export const postUserRegister = async ({ userEmail, password }) => { From cb9ed32d6ef5ded6695f36237276f46fff07559a Mon Sep 17 00:00:00 2001 From: saseungmin Date: Tue, 8 Dec 2020 22:12:20 +0900 Subject: [PATCH 2/2] [Feature] Cancellation of study application acceptance - Cancellation is possible only during the application period --- src/components/common/DateTimeChange.jsx | 2 +- src/components/common/DateTimeChange.test.jsx | 4 +- .../introduce/ApplyStatusButton.jsx | 30 ++++++++++---- .../introduce/ApplyStatusButton.test.jsx | 10 ++++- src/components/introduce/IntroduceHeader.jsx | 3 +- .../introduce/IntroduceHeader.test.jsx | 32 +++++++++++++++ .../introduce/IntroduceContainer.jsx | 9 ++++- .../introduce/IntroduceContainer.test.jsx | 39 ++++++++++++++++++- 8 files changed, 114 insertions(+), 15 deletions(-) diff --git a/src/components/common/DateTimeChange.jsx b/src/components/common/DateTimeChange.jsx index 0e77ca8..a4da9c2 100644 --- a/src/components/common/DateTimeChange.jsx +++ b/src/components/common/DateTimeChange.jsx @@ -44,7 +44,7 @@ const DateTimeChange = ({ group, page, time }) => { if (isCheckedTimeStatus(valid)) { return ( - 모집마감 + 모집 마감 ); } diff --git a/src/components/common/DateTimeChange.test.jsx b/src/components/common/DateTimeChange.test.jsx index 37c6a93..868e160 100644 --- a/src/components/common/DateTimeChange.test.jsx +++ b/src/components/common/DateTimeChange.test.jsx @@ -133,7 +133,7 @@ describe('DateTimeChange', () => { const { container } = renderDateTimeChange({ group, page, time }); - expect(container).toHaveTextContent('모집마감'); + expect(container).toHaveTextContent('모집 마감'); }); }); @@ -153,7 +153,7 @@ describe('DateTimeChange', () => { const { container } = renderDateTimeChange({ group, page, time }); - expect(container).toHaveTextContent('모집마감'); + expect(container).toHaveTextContent('모집 마감'); }); }); }); diff --git a/src/components/introduce/ApplyStatusButton.jsx b/src/components/introduce/ApplyStatusButton.jsx index a88dceb..0f7c301 100644 --- a/src/components/introduce/ApplyStatusButton.jsx +++ b/src/components/introduce/ApplyStatusButton.jsx @@ -22,6 +22,15 @@ const ApplyStatusButtonWrapper = styled.button` color: ${palette.gray[5]}; } + &.apply-cancel { + cursor: pointer; + background: ${palette.orange[4]}; + color: white; + &:hover { + background: ${palette.orange[3]}; + } + } + &.apply-complete { background: ${palette.gray[1]}; border: 2px solid #a5d8ff; @@ -32,20 +41,27 @@ const ApplyStatusButtonWrapper = styled.button` color: white; cursor: pointer; background: ${palette.teal[5]}; - &:hover{ + &:hover { background: ${palette.teal[4]}; } } - - &.no-login{ - cursor: not-allowed; - color: ${palette.gray[5]}; - } `; const ApplyStatusButton = ({ - timeStatus, onApply, applyStatus, + timeStatus, onApply, applyStatus, onCancel, }) => { + if (!timeStatus && applyStatus) { + return ( + + 신청 취소 + + ); + } + if (applyStatus) { return ( { /> )); + context('When the applicant applies before the application deadline', () => { + it('renders Cancel application', () => { + const { container } = renderApplyStatusButton({ applyStatus: true }); + + expect(container).toHaveTextContent('신청 취소'); + }); + }); + context('When the study application is completed', () => { it('renders application completed', () => { - const { container } = renderApplyStatusButton({ applyStatus: true }); + const { container } = renderApplyStatusButton({ applyStatus: true, timeStatus: true }); expect(container).toHaveTextContent('신청 완료'); }); diff --git a/src/components/introduce/IntroduceHeader.jsx b/src/components/introduce/IntroduceHeader.jsx index 593cf9c..940303a 100644 --- a/src/components/introduce/IntroduceHeader.jsx +++ b/src/components/introduce/IntroduceHeader.jsx @@ -22,7 +22,7 @@ const IntroduceHeaderWrapper = styled.div` `; const IntroduceHeader = ({ - group, onApply, user, realTime, + group, onApply, user, realTime, onApplyCancel, }) => { const [modal, setModal] = useState(false); @@ -56,6 +56,7 @@ const IntroduceHeader = ({ <> { const handleApply = jest.fn(); + const handleApplyCancel = jest.fn(); beforeEach(() => { jest.clearAllMocks(); @@ -19,6 +20,7 @@ describe('IntroduceHeader', () => { group={group} realTime={time} onApply={handleApply} + onApplyCancel={handleApplyCancel} /> )); @@ -28,6 +30,36 @@ describe('IntroduceHeader', () => { expect(container).toHaveTextContent('스터디를 소개합니다.2'); }); + context(`When the application date is earlier than the + deadline date and the application deadline is not reached`, () => { + const time = Date.now(); + + const nowDate = new Date(); + const tomorrow = nowDate.setDate(nowDate.getDate() + 1); + + const group = { + ...STUDY_GROUP, + applyEndDate: tomorrow, + participants: [ + 'user2', + 'user', + ], + personnel: 3, + }; + + it('Call the cancel application button.', () => { + const { getByText } = renderIntroduceHeader({ group, user: 'user', time }); + + const button = getByText('신청 취소'); + + expect(button).not.toBeNull(); + + fireEvent.click(button); + + expect(handleApplyCancel).toBeCalled(); + }); + }); + context('When the author and the logged-in user have the same ID', () => { it("doesn't renders apply button", () => { const { container } = renderIntroduceHeader({ group: STUDY_GROUP, user: 'user2' }); diff --git a/src/containers/introduce/IntroduceContainer.jsx b/src/containers/introduce/IntroduceContainer.jsx index 6d1c12a..b0222e1 100644 --- a/src/containers/introduce/IntroduceContainer.jsx +++ b/src/containers/introduce/IntroduceContainer.jsx @@ -4,7 +4,7 @@ import { useInterval } from 'react-use'; import { useDispatch, useSelector } from 'react-redux'; import { getAuth, getGroup } from '../../util/utils'; -import { loadStudyGroup, updateParticipant } from '../../reducers/groupSlice'; +import { deleteParticipant, loadStudyGroup, updateParticipant } from '../../reducers/groupSlice'; import StudyIntroduceForm from '../../components/introduce/StudyIntroduceForm'; import GroupContentLoader from '../../components/introduce/GroupsContentLoader'; @@ -30,6 +30,12 @@ const IntroduceContainer = ({ groupId }) => { dispatch(updateParticipant()); }, [dispatch]); + const onApplyCancel = useCallback(() => { + if (user) { + dispatch(deleteParticipant()); + } + }, [dispatch, user]); + if (!group) { return ( @@ -43,6 +49,7 @@ const IntroduceContainer = ({ groupId }) => { group={group} realTime={realTime} onApply={onApplyStudy} + onApplyCancel={onApplyCancel} /> { }); }); - context('without group', () => { + context('without group ', () => { given('group', () => (null)); it('renders "loading.." text', () => { @@ -73,13 +73,15 @@ describe('IntroduceContainer', () => { }); }); - describe('with user', () => { + context('with group & user', () => { given('group', () => (STUDY_GROUP)); given('user', () => ('user')); it('click event dispatches action call updateParticipant', () => { const { getByText } = renderIntroduceContainer(1); + expect(dispatch).toBeCalledTimes(1); + const button = getByText('신청하기'); expect(button).not.toBeNull(); @@ -89,4 +91,37 @@ describe('IntroduceContainer', () => { expect(dispatch).toBeCalledTimes(2); }); }); + + describe(`When the application date is earlier than the deadline + date and the application deadline is not reached`, () => { + const nowDate = new Date(); + const tomorrow = nowDate.setDate(nowDate.getDate() + 1); + + const group = { + ...STUDY_GROUP, + applyEndDate: tomorrow, + participants: [ + 'user2', + 'user', + ], + personnel: 3, + }; + + given('group', () => (group)); + given('user', () => ('user')); + + it('click event dispatches action call deleteParticipant', () => { + const { getByText } = renderIntroduceContainer(1); + + expect(dispatch).toBeCalledTimes(1); + + const button = getByText('신청 취소'); + + expect(button).not.toBeNull(); + + fireEvent.click(button); + + expect(dispatch).toBeCalledTimes(2); + }); + }); });