diff --git a/src/App.test.jsx b/src/App.test.jsx
index 22a4a58..dcc42d6 100644
--- a/src/App.test.jsx
+++ b/src/App.test.jsx
@@ -24,6 +24,9 @@ describe('App', () => {
useSelector.mockImplementation((selector) => selector({
groups: STUDY_GROUPS,
group: STUDY_GROUP,
+ writeField: {
+ tags: [],
+ },
}));
});
diff --git a/src/components/write/TagsForm.jsx b/src/components/write/TagsForm.jsx
new file mode 100644
index 0000000..1e72887
--- /dev/null
+++ b/src/components/write/TagsForm.jsx
@@ -0,0 +1,73 @@
+import React, { useEffect, useState } from 'react';
+
+import styled from '@emotion/styled';
+
+const TagsFormWrapper = styled.div``;
+
+const TagsForm = ({ onChange, tags }) => {
+ const [tag, setTag] = useState('');
+ const [inputTags, setInputTags] = useState([]);
+
+ const validateInput = (value) => {
+ if (!value) {
+ return;
+ }
+
+ if (inputTags.includes(value)) {
+ return;
+ }
+
+ const resultTags = [...inputTags, value];
+
+ setInputTags(resultTags);
+ onChange(resultTags);
+ };
+
+ const handleChange = (e) => {
+ setTag(e.target.value);
+ };
+
+ const handleRemove = (removeTag) => {
+ const removeTags = inputTags.filter((value) => value !== removeTag);
+
+ setInputTags(removeTags);
+ onChange(removeTags);
+ };
+
+ const handleSubmit = () => {
+ validateInput(tag.trim());
+ setTag('');
+ };
+
+ useEffect(() => {
+ setInputTags(tags);
+ }, [tags]);
+
+ return (
+
+ 태그
+
+
+ {inputTags.map((inputTag) => (
+ handleRemove(inputTag)}
+ onKeyPress={() => {}}
+ role="button"
+ tabIndex="-1"
+ >
+ {`#${inputTag}`}
+
+ ))}
+
+
+ );
+};
+
+export default TagsForm;
diff --git a/src/components/write/TagsForm.test.jsx b/src/components/write/TagsForm.test.jsx
new file mode 100644
index 0000000..f052342
--- /dev/null
+++ b/src/components/write/TagsForm.test.jsx
@@ -0,0 +1,117 @@
+import React from 'react';
+
+import { fireEvent, render } from '@testing-library/react';
+
+import TagsForm from './TagsForm';
+
+describe('TagsForm', () => {
+ const handleChange = jest.fn();
+
+ beforeEach(() => {
+ handleChange.mockClear();
+ });
+
+ const renderTagsTagsForm = (tags) => render((
+
+ ));
+
+ describe('render Tag Form Container contents text', () => {
+ it('renders tag form text', () => {
+ const { getByPlaceholderText, container } = renderTagsTagsForm([]);
+
+ expect(getByPlaceholderText('태그를 입력하세요')).not.toBeNull();
+ expect(container).toHaveTextContent('태그');
+ });
+ });
+
+ describe('Check input tag validate', () => {
+ context('with tag input value', () => {
+ const tags = ['JavaScript', 'React'];
+ it('listens write field change events', () => {
+ const { getByPlaceholderText } = renderTagsTagsForm(['CSS']);
+
+ const input = getByPlaceholderText('태그를 입력하세요');
+
+ tags.forEach((tag) => {
+ fireEvent.change(input, { target: { value: tag } });
+
+ fireEvent.keyPress(input, { key: 'Enter', code: 13, charCode: 13 });
+
+ expect(input).toHaveValue('');
+ });
+ expect(handleChange).toBeCalledTimes(2);
+ });
+ });
+
+ context('without tag input value', () => {
+ const tags = [];
+ it("doesn't listens write field change events", () => {
+ const { getByPlaceholderText } = renderTagsTagsForm(tags);
+
+ const input = getByPlaceholderText('태그를 입력하세요');
+
+ fireEvent.change(input, { target: { value: '' } });
+
+ fireEvent.keyPress(input, { key: 'Enter', code: 13, charCode: 13 });
+
+ expect(handleChange).not.toBeCalled();
+ });
+ });
+
+ describe('It is not executed because the current tag value is included.', () => {
+ const tags = ['JavaScript', 'React'];
+ it('listens write field change events', () => {
+ const { getByPlaceholderText } = renderTagsTagsForm(tags);
+
+ const input = getByPlaceholderText('태그를 입력하세요');
+
+ tags.forEach((tag) => {
+ fireEvent.change(input, { target: { value: tag } });
+
+ fireEvent.keyPress(input, { key: 'Enter', code: 13, charCode: 13 });
+
+ expect(input).toHaveValue('');
+
+ expect(handleChange).not.toBeCalled();
+ });
+ });
+ });
+ });
+
+ context('with tags', () => {
+ const tags = ['JavaScript', 'React'];
+
+ it('renders Tags are below the input window', () => {
+ const { container } = renderTagsTagsForm(tags);
+
+ expect(container).toHaveTextContent('#JavaScript');
+ expect(container).toHaveTextContent('#React');
+ });
+
+ it('Click event remove tag', () => {
+ const { getByText, container } = renderTagsTagsForm(tags);
+
+ tags.forEach((tag) => {
+ fireEvent.click(getByText(`#${tag}`));
+
+ expect(container).not.toHaveTextContent(`#${tag}`);
+ });
+
+ expect(handleChange).toBeCalledTimes(2);
+ });
+ });
+
+ context('without tags', () => {
+ const tags = [];
+
+ it('renders Tags are below the input window', () => {
+ const { container } = renderTagsTagsForm(tags);
+
+ expect(container).not.toHaveTextContent('#JavaScript');
+ expect(container).not.toHaveTextContent('#React');
+ });
+ });
+});
diff --git a/src/containers/write/TagsFormContainer.jsx b/src/containers/write/TagsFormContainer.jsx
new file mode 100644
index 0000000..aa7db8b
--- /dev/null
+++ b/src/containers/write/TagsFormContainer.jsx
@@ -0,0 +1,28 @@
+import React from 'react';
+
+import { useDispatch, useSelector } from 'react-redux';
+
+import { get } from '../../util/utils';
+import TagsForm from '../../components/write/TagsForm';
+import { changeWriteField } from '../../reducers/slice';
+
+const TagsFormContainer = () => {
+ const dispatch = useDispatch();
+
+ const { tags } = useSelector(get('writeField'));
+
+ const onChangeTags = (nextTags) => {
+ dispatch(
+ changeWriteField({
+ name: 'tags',
+ value: nextTags,
+ }),
+ );
+ };
+
+ return (
+
+ );
+};
+
+export default TagsFormContainer;
diff --git a/src/containers/write/TagsFormContainer.test.jsx b/src/containers/write/TagsFormContainer.test.jsx
new file mode 100644
index 0000000..ead8238
--- /dev/null
+++ b/src/containers/write/TagsFormContainer.test.jsx
@@ -0,0 +1,57 @@
+import React from 'react';
+
+import { fireEvent, render } from '@testing-library/react';
+
+import { useDispatch, useSelector } from 'react-redux';
+
+import TagsFormContainer from './TagsFormContainer';
+
+describe('TagsFormContainer', () => {
+ const dispatch = jest.fn();
+
+ beforeEach(() => {
+ dispatch.mockClear();
+
+ useDispatch.mockImplementation(() => dispatch);
+
+ useSelector.mockImplementation((state) => state({
+ writeField: {
+ tags: [],
+ },
+ }));
+ });
+
+ const renderTagsFormContainer = () => render((
+
+ ));
+
+ describe('render Tag Form Container contents text', () => {
+ it('renders tag form text', () => {
+ const { getByPlaceholderText, container } = renderTagsFormContainer();
+
+ expect(getByPlaceholderText('태그를 입력하세요')).not.toBeNull();
+ expect(container).toHaveTextContent('태그');
+ });
+ });
+
+ describe('calls dispatch tags change action', () => {
+ const tags = ['JavaScript', 'React'];
+ it('change tags', () => {
+ const { getByPlaceholderText } = renderTagsFormContainer();
+
+ const input = getByPlaceholderText('태그를 입력하세요');
+
+ tags.forEach((tag) => {
+ fireEvent.change(input, { target: { value: tag } });
+
+ fireEvent.keyPress(input, { key: 'Enter', code: 13, charCode: 13 });
+
+ expect(input).toHaveValue('');
+ });
+ expect(dispatch).toBeCalledWith({
+ type: 'application/changeWriteField',
+ payload: { name: 'tags', value: tags },
+ });
+ });
+ });
+});
diff --git a/src/pages/WritePage.jsx b/src/pages/WritePage.jsx
index 363e710..64f616b 100644
--- a/src/pages/WritePage.jsx
+++ b/src/pages/WritePage.jsx
@@ -1,5 +1,7 @@
import React from 'react';
+import TagFormContainer from '../containers/write/TagsFormContainer';
+
import Responsive from '../styles/Responsive';
const IntroducePage = () => (
@@ -9,11 +11,17 @@ const IntroducePage = () => (
-
+ 모집 마감 날짜
+
-
+ 참여 인원 수
+
+
+
+
+
diff --git a/src/pages/WritePage.test.jsx b/src/pages/WritePage.test.jsx
index 4796660..f410059 100644
--- a/src/pages/WritePage.test.jsx
+++ b/src/pages/WritePage.test.jsx
@@ -1,10 +1,26 @@
import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+
import { render } from '@testing-library/react';
import WritePage from './WritePage';
describe('WritePage', () => {
+ const dispatch = jest.fn();
+
+ beforeEach(() => {
+ dispatch.mockClear();
+
+ useDispatch.mockImplementation(() => dispatch);
+
+ useSelector.mockImplementation((state) => state({
+ writeField: {
+ tags: [],
+ },
+ }));
+ });
+
const renderWritePage = () => render((
));
@@ -21,8 +37,14 @@ describe('WritePage', () => {
expect(getByPlaceholderText('제목을 입력하세요')).not.toBeNull();
expect(getByPlaceholderText('내용')).not.toBeNull();
- expect(getByPlaceholderText('태그를 입력하세요')).not.toBeNull();
expect(getByText('저장')).not.toBeNull();
});
+
+ it('renders tag form text', () => {
+ const { getByPlaceholderText, container } = renderWritePage();
+
+ expect(getByPlaceholderText('태그를 입력하세요')).not.toBeNull();
+ expect(container).toHaveTextContent('태그');
+ });
});
});
diff --git a/src/reducers/slice.js b/src/reducers/slice.js
index 651224e..78a5348 100644
--- a/src/reducers/slice.js
+++ b/src/reducers/slice.js
@@ -5,11 +5,22 @@ import {
getStudyGroups,
} from '../services/api';
+const writeInitialState = {
+ title: '',
+ contents: '',
+ moderatorId: '',
+ applyEndDate: '',
+ participants: [],
+ personnel: 0,
+ tags: [],
+};
+
const { actions, reducer } = createSlice({
name: 'application',
initialState: {
groups: [],
group: null,
+ writeField: writeInitialState,
},
reducers: {
setStudyGroups(state, { payload: { groups, tag } }) {
@@ -30,12 +41,22 @@ const { actions, reducer } = createSlice({
group,
};
},
+ changeWriteField(state, { payload: { name, value } }) {
+ return {
+ ...state,
+ writeField: {
+ ...state.writeField,
+ [name]: value,
+ },
+ };
+ },
},
});
export const {
setStudyGroups,
setStudyGroup,
+ changeWriteField,
} = actions;
export const loadStudyGroups = (tag) => async (dispatch) => {
diff --git a/src/reducers/slice.test.js b/src/reducers/slice.test.js
index 4935970..b98380d 100644
--- a/src/reducers/slice.test.js
+++ b/src/reducers/slice.test.js
@@ -8,6 +8,7 @@ import reducer, {
loadStudyGroups,
setStudyGroup,
loadStudyGroup,
+ changeWriteField,
} from './slice';
import STUDY_GROUPS from '../../fixtures/study-groups';
@@ -23,6 +24,15 @@ describe('reducer', () => {
const initialState = {
groups: [],
group: null,
+ writeField: {
+ title: '',
+ contents: '',
+ moderatorId: '',
+ applyEndDate: '',
+ participants: [],
+ personnel: 0,
+ tags: [],
+ },
};
it('returns initialState', () => {
@@ -75,6 +85,29 @@ describe('reducer', () => {
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: 0,
+ tags: [],
+ },
+ };
+
+ const state = reducer(
+ initialState,
+ changeWriteField({ name: 'tags', value: ['JavaScript', 'React'] }),
+ );
+
+ expect(state.writeField.tags).toEqual(['JavaScript', 'React']);
+ });
+ });
});
describe('async actions', () => {