화면구현 1의 내용 가져오기
화면구현1에 사용된 라이브러리를 모두 추가한다.
1 |
npm install axios node-sass react-router-dom @types/react-router-dom |
package.json에 proxy를 설정한다.
1 2 3 4 |
... }, "proxy": "http://localhost:8080" } |
화면구현1에서 구현한 모든 소스들을 가져온다.
- components 폴더: CommentList.scss, CommentList.tsx
- dto 폴더: Board.ts, Comment.ts
- pages/board-edit: BoardEdit.tsx
- pages/board-list: BoardList.scss, BoardList.tsx
- pages/board-register: BoardRegister.tsx
- pages/board-view: BoardView.tsx
컴포넌트에서 리턴하는 jsx 부분은 bootstrap에서 ant 로 대체될 것이므로 비워둔다. 그리고 bootstrap을 import 하는 부분도 삭제한다. 모든 컴포넌트에 공통으로 적용한다.
아래 BoardList.tsx를 보면 return 부분이 비워져 있고 상단 bootstrap 부분이 삭제되었다. 모든 컴포넌트에 적용이 되었다면 에러가 없어야 한다.
라우팅 적용
App.tsx에 라우팅을 적용한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function App() { return ( <layout> <header>Header</header> <content> <browserrouter> <switch> <route exact="" path="/" component="{BoardList}"></route> <route path="/board-register" component="{BoardRegister}"></route> <route path="/board-view/:id" component="{BoardView}"></route> <route path="/board-edit/:id" component="{BoardEdit}"></route> </switch> </browserrouter> </content> <footer>Footer</footer> </layout> ); } |
게시판 목록 보기
ant 디자인의 그리드 컴포넌트와 Button 컴포넌트를 참고한다.
ant에는 bootstrap에 있는 마진과 패딩의 유틸리티 클래스가 존재하지 않는다. 필요하면 scss 문법으로 만들어 사용할수도 있다. bootstrap에서 만들어진 유틸리티 클래스를 import해서 사용하겠다. 여기서 bootstrap-4-utilities.css 화일을 다운로드 받아서 css 폴더를 만들고 그 안에 카피한다. 그리고 App.less 상단에서 import 한다.
1 2 3 |
@import "css/bootstrap-4-utilities.css"; @import '~antd/dist/antd.less'; ... |
BoardList.scss화일은 화면구현1과 변경사항이 없다.
1 2 3 4 5 6 7 |
.board { cursor: pointer; border-bottom: 1px solid #dddddd; &:hover { background: #eeeeee; } } |
BoardList.tsx의 화면은 ant 디자인으로 구성한다. ant에서 Row는 24칸으로 구성되어있다. Row 컴포넌트에는 다양한 속성이 있으므로 홈페이지를 참고한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
const BoardList: React.FC = (props: any) => { // console.log(props); const [boardList, setBoardList] = useState([]); useEffect(() => { getBoardList(); }, []); const getBoardList = async () => { // res는 http response의 header + body를 모두 갖고 있다. const res = await axios.get('/api/boards'); console.log(res); setBoardList(res.data); } return ( <> <Row justify="end" className="mb-3"> <Col> <Button type="primary" onClick={() => props.history.push('/board-register')}>등 록</Button> </Col> </Row> { boardList.map((board: Board)=> <Row justify="space-between" className="board py-2" onClick={() => props.history.push(`/board-view/${board.id}`)}> <Col span={18}>{board.title}</Col> <Col> <span>{board.created}</span> </Col> </Row>) } </> ); }; |
게시판 등록 화면
화면을 개발할 때 가장 많이 다루는 부분이 Form과 관련된 부분이다. UI 뿐만 아니라 validation 체크까지해야 하므로 시간이 많이 걸리는 부분이기도 하다. bootstrap은 form을 유연하게 처리하기 위해서 별도의 라이브러리가 필요했지만 ant에서는 대부분의 라이브러리가 갖추어져 있다. 따라서 bootstrap에서 했던 많은 코드들이 간략히 처리될 수 있다.
useForm이라는 훅을 사용하여 Form 안의 객체들에 값을 가져올 수도 있고 값을 설정할 수도 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
const BoardRegister: React.FC = (props: any) => { const [form] = Form.useForm(); const handleSubmit = (values: any) => { console.log(values); addBoard(values); }; const addBoard = async (board: Board) => { const res = await axios.post('/api/board', board); console.log(res); if (res.status >= 200 && res.status < 300) { message.success('등록되었습니다.'); props.history.push('/'); } else { message.error('error happened.') } } return ( <> <Form name="boardForm" layout="vertical" form={form} requiredMark={true} onFinish={handleSubmit} > <Form.Item label="제목" name="title" rules={[ { required: true, message: '제목을 입력하세요', } ]}> <Input /> </Form.Item> <Form.Item label="내용" name="content" rules={[ { required: true, message: '내용을 입력하세요', } ]}> <Input.TextArea rows={15} /> </Form.Item> <Form.Item> <Button type="primary" htmlType="submit">Submit</Button> </Form.Item> </Form> </> ); }; |
게시판 상세보기 및 삭제
bootstrap에는 없는 다양한 UI 컴포넌트들이 존재한다. 모달의 경우 동적으로 생성해서 띄울수 잇고 토스트형태의 컴포넌트인 messag라는 컴포넌트가 존재해서 처리 결과를 message 컴포넌트로 알려줄 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
const BoardView = ({match, history}: any) => { const [board, setBoard] = useState<Board>({ title: '', content: '' }); useEffect(() => { console.log(match); getBoard(match.params.id); }, []); const getBoard = async (id: string) => { const res = await axios.get(`/api/board/${id}`); console.log(res.data); setBoard(res.data); } const confirmDelete = () => { Modal.confirm({ title: '삭제', icon: <ExclamationCircleOutlined />, content: '삭제하시겠습니까?', okText: 'OK', cancelText: 'Cancel', onOk: handleDelete }); } const handleDelete = async () => { const res = await axios.delete(`/api/board?id=${match.params.id}`); if (res.status >= 200 && res.status < 300) { message.success('삭제되었습니다.'); history.goBack(); } else { message.error('error happened.') } } return ( <> <Row justify="end" className="mb-3"> <Button type="primary" onClick={() => history.push(`/board-edit/${match.params.id}`)} className="mr-2">수정</Button> <Button type="primary" danger onClick={() => confirmDelete()}>삭제</Button> </Row> <Card title={board.title}> <p>{board.content}</p> </Card> <Row justify="center" className="mt-3"> <Button type="primary" ghost onClick={() => history.goBack()}>돌아가기</Button> </Row> <CommentList board_id={match.params.id}></CommentList> </> ); }; |
게시판 수정
form에 값을 설정하기 위해서 bootstrap에서는 DOM으로 접근했지만 useForm 훅으로 만든 form 변수에 setFieldValue 메서드로 form의 값을 설정할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
const BoardEdit: React.FC = ({match, history}: any) => { const [form] = Form.useForm(); useEffect(() => { console.log(match); getBoard(match.params.id); }, []); const getBoard = async (id: string) => { const res = await axios.get(`/api/board/${id}`); console.log(res.data); form.setFieldsValue({ title: res.data.title, content: res.data.content, }); } const handleSubmit = (values: any) => { updateBoard(values); }; const updateBoard = async (board: Board) => { const res = await axios.put('/api/board', board); console.log(res); if (res.status >= 200 && res.status < 300) { message.success('수정되었습니다.'); history.push('/'); } else { message.error('error happened.') } } return ( <> <Form name="boardForm" layout="vertical" form={form} requiredMark={true} onFinish={handleSubmit} > <Form.Item label="제목" name="title" rules={[ { required: true, message: '제목을 입력하세요', } ]}> <Input /> </Form.Item> <Form.Item label="내용" name="content" rules={[ { required: true, message: '내용을 입력하세요', } ]}> <Input.TextArea rows={15} /> </Form.Item> <Form.Item> <Button type="primary" htmlType="submit">Submit</Button> </Form.Item> </Form> </> ); }; |
댓글 보기 및 등록
앞에서 댓글 컴포넌트를 직접 작성하였는데, ant에 Comment라는 컴포넌트가 존재한다. 그 컴포넌트를 사용하여 작성해보자. 좋아요, 싫어요 와 댓글에 답변 달기는 서버설계가 반영되어야 하는데 여기서는 UI만 구현되어있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
const CommentList: React.FC<Props> = (props) => { const [form] = Form.useForm(); const [comments, setComments] = useState<Array<Comment>>([]); const [submitting, setSubmitting] = useState(false); const [myLike, setMyLike] = useState(0); useEffect(() => { if (!props.board_id) { return; } getComments(props.board_id); }, [props.board_id]); const getComments = async (board_id: number) => { const res = await axios.get(`/api/comments?board_id=${props.board_id}`); setComments(res.data); } const handleSubmit = async (values: any) => { console.log(values); const comment = { board_id: props.board_id, content: values.content } setSubmitting(true); let res = await axios.post('/api/comment', comment); console.log(res); res = await axios.get(`/api/comment?id=${res.data.id}`); const newComments = [...comments]; newComments.unshift(res.data); setComments(newComments); setSubmitting(false); }; const like = (id: number) => { // 서버에 좋아요 추가/수정 후 전체 카운트 갱신 setMyLike(1); } const dislike = (id: number) => { // 서버에 싫어요 추가/수정 후 전체 카운트 갱신 setMyLike(-1); } return ( <> {comments.length > 0 && <List className="comment-list" header={`${comments.length} replies`} itemLayout="horizontal" dataSource={comments} renderItem={(comment: any) => ( <li> <Comment actions={[ <Tooltip key="comment-basic-like" title="Like"> <span onClick={() => like(comment.id)}> {createElement(myLike === 1 ? LikeFilled : LikeOutlined)} <span className="comment-action">30</span> </span> </Tooltip>, <Tooltip key="comment-basic-dislike" title="Dislike"> <span onClick={() => dislike(comment.id)}> {React.createElement(myLike === -1 ? DislikeFilled : DislikeOutlined)} <span className="comment-action">20</span> </span> </Tooltip>, <span key="comment-basic-reply-to">Reply to</span>, ]} author={'user name'} avatar={<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" alt="Han Solo" />} content={<p> {comment.content} </p>} datetime={comment.created} /> </li> )} /> } <Comment content={ <Form form={form} onFinish={handleSubmit}> <Form.Item name="content" rules={[ { required: true, message: '제목을 입력하세요', } ]}> <Input.TextArea rows={4} /> </Form.Item> <Form.Item> <Button htmlType="submit" loading={submitting} type="primary"> Add Comment </Button> </Form.Item> </Form> }> </Comment> </> ); } |