[ 구현 사항 ]
1)
/
,/members
,/members/:id
페이지 라우팅 설정
2) Grid를 이용한 Card배치
3) 데이터 로딩중 설정
4) 게시글 CRUD(Create / Read / Update / Delete)
: 내용이 비어있는 Card를 추가하는 기능
(MemberList.js)
... /* 빈 카드를 클릭시 발생하는 함수 */ const onCreateCard = async () => { /* 빈 내용을 담는 object 생성 */ const object = { "name": "", "profileUrl": "", "introduction": "", "mbti": "", "instagram": "" }; try{ /* 서버에 빈 객체를 생성하는 api요청 */ const result = await createMember(object); /* 성공시에 반환된 빈 내용을 현재 members에 추가! */ setMembersState({ members: [...membersState.members, result], status: 'resolved' }); }catch(e){ setMembersState({members: membersState.members, status: 'rejected'}); } } ... <div className="member-list-content-wrapper"> {membersState.members.map((member, i) => <Card key={"card-" + i} memberData={member} onRemoveCard={removeCard} />)} /* CardEmpty 클릭시 새로운 카드 생성 */ <CardEmpty onClick={onCreateCard}> <CardEmptyText>+ New</CardEmptyText> </CardEmpty> </div> ...
: members.map을 통해 여러 Card를 출력하는 마지막에 빈 카드 추가!
그리고 onCreateCard라는 이벤트 처리 함수에서 서버와 통신
(memberAPI.js)
... const createMember = async(object) => { try { const { data } = await axios.post(`${url}`,object); console.log('[SUCCESS] CREATE MEMBER', data); return data.data; } catch (e) { console.error('[FAIL] CREATE MEMBER', e); throw(e); } } export { getMembers, createMember, };
: 넘겨받은 빈 객체를 서버에 보내서 생성하는 코드
[ 결과물 ]
: 특정 Card클릭시 MemberDetail로 이동해서 자세한 해당 내용을 보여주는 기능
(Card.js)
import './Card.scss'; import { DeleteOutlined, FileImageOutlined } from '@ant-design/icons'; import {withRouter} from 'react-router-dom'; function Card({ history, match, memberData }) { return ( /* onClick으로 클릭할 때 /members/:id로 이동하게 한다. */ <div className="card" onClick={() => history.push(`${match.path}/${memberData.id}`)} draggable > <div className="remove-button" onClick> <DeleteOutlined style={{ fontSize: "16px"}}/> </div> <div className="image-area"> { memberData.profileUrl !== '' ? <img src={memberData.profileUrl} alt="profile"></img> : <FileImageOutlined style={{fontSize: "40px"}}/> } </div> <div className="card__content card__text name">{memberData.name}</div> <div className="card__content card__text instagram">{memberData.instagram}</div> <div className="card__content card__text introduction">{memberData.introduction}</div> { memberData.mbti && <span className="card__content card__text mbti">{memberData.mbti}</span> } </div> ); } export default withRouter(Card);
: 해당 컴포넌트는 직접 Route되지 않았기 때문에 withRouter를 사용하여
history와 match를 가져와서 Redirect하였다
(MemberDetail.js)
import React, {useState, useEffect} from 'react' import { getMember, updateMember } from '../../lib/api/memberAPI'; import Button from '../../components/button/Button'; import Loading from '../../components/loading/Loading'; import { Input } from 'antd'; import { InstagramOutlined, AlignLeftOutlined, RadarChartOutlined } from '@ant-design/icons'; import './MemberDetail.scss' function MemberDetail({match}) { const [ memberState, setMemberState ] = useState({ status: 'idle', member: null }); /* Component가 mount되었을 때 id에 해당하는 정보를 서버에서 받아오는 코드 */ useEffect(() => { (async () => { try { setMemberState({ status: 'pending', member: null }); /* url에서 params인 id를 빼서 getMember에 넘겨준다 */ const result = await getMember(match.params.id); /* setTimeout을 쓴 이유는 그냥 로딩중이 너무 빨리지나가서 아쉬워서; */ setTimeout(() => setMemberState({ status: 'resolved', member: result }), 800); } catch (e) { setMemberState({ status: 'rejected', member: null }); } })(); }, []); /* 역시 MemberList.js처럼 조건부 렌더링을 해서 Loading중 처리! */ switch (memberState.status) { case 'pending': return <Loading />; case 'rejected': return <div>데이터 로드 실패!</div> case 'resolved': return ( <> <div className="member-detail"> <div className="member-detail__button-area"> <Button text="Add icon"></Button> <Button text="Add cover"></Button> </div> <Input className="member-detail__content name" bordered={false} name="name" value={memberState.member.name} onChange/> <hr style={{borderTop: "solid 1px #eee", marginBottom: "24px"}}/> <div className="member-detail__content"> <div className="content-title"><InstagramOutlined /> 인스타 아이디</div> <Input className="content-input" bordered={false} name="instagram" value={memberState.member.instagram} onChange/> </div> <div className="member-detail__content"> <div className="content-title"><AlignLeftOutlined /> 한 줄 소개</div> <Input className="content-input oneline" bordered={false} name="introduction" value={memberState.member.introduction} onChange/> </div> <div className="member-detail__content"> <div className="content-title"><RadarChartOutlined /> mbti</div> <Input className="content-input" bordered={false} name="mbti" value={memberState.member.mbti} onChange/> </div> <div className="member-detail__content"> { memberState.member.profileUrl !== '' ? <img className="content-image" src={memberState.member.profileUrl} alt={'profile url'} /> : '' } </div> </div> </> ); case 'idle': default : return <div>idle 입니다</div> } } export default MemberDetail
: match.params를 통해서 url에 있는 id라는 params를 가져온 뒤
서버와 통신하여 해당 정보를 저장하고 출력
(memberAPI.js)
... const getMember = async (id) => { try { const { data } = await axios.get(`${url}/${id}`); console.log('[SUCCESS] GET MEMBER', data); return data.data; } catch (e) { console.error('[FAIL] GET MEMBER', e); throw(e); } } export { getMembers, getMember, createMember, };
: 전달받은 id를 통해 서버에서 정보를 받아온 뒤 response!
[ 결과물 ]
: MemberDetail 페이지에서 각 정보를 클릭하여 수정하면 바로바로 서버와 통신 후
내용이 저장되는 기능
(MemberDetail.js)
... /* onChange 이벤트 함수 작성 */ const onChangeInputs = async (evt) =>{ /* input태그이기 때문에 바로 evt.target에서 name을 가져올 수 있다 div였다면 evt.target.attributes에서 가져와야 했을 것이다 */ const {name, value} = evt.target; const result = await updateMember(match.params.id, { ...memberState.member, /* 이 부분으로 name과 value를 각각 매칭 해주어서 효율적인 코드가 가능한 것! */ [name]: value }); setMemberState({ status: 'resolved', member: result }); } switch (memberState.status) { case 'pending': return <Loading />; case 'rejected': return <div>데이터 로드 실패!</div> case 'resolved': return ( <> <div className="member-detail"> <div className="member-detail__button-area"> <Button text="Add icon"></Button> <Button text="Add cover"></Button> </div> <Input className="member-detail__content name" bordered={false} name="name" value={memberState.member.name} onChange={onChangeInputs}/> <hr style={{borderTop: "solid 1px #eee", marginBottom: "24px"}}/> <div className="member-detail__content"> <div className="content-title"><InstagramOutlined /> 인스타 아이디</div> <Input className="content-input" bordered={false} name="instagram" value={memberState.member.instagram} onChange={onChangeInputs}/> </div> <div className="member-detail__content"> <div className="content-title"><AlignLeftOutlined /> 한 줄 소개</div> <Input className="content-input oneline" bordered={false} name="introduction" value={memberState.member.introduction} onChange={onChangeInputs}/> </div> <div className="member-detail__content"> <div className="content-title"><RadarChartOutlined /> mbti</div> <Input className="content-input" bordered={false} name="mbti" value={memberState.member.mbti} onChange={onChangeInputs}/> </div> <div className="member-detail__content"> { memberState.member.profileUrl !== '' ? <img className="content-image" src={memberState.member.profileUrl} alt={'profile url'} /> : '' } </div> </div> </> ); case 'idle': default : return <div>idle 입니다</div> } }
: name / instagram / introduction / mbti 에 onChange를 작성!
각 요소가 바뀔 때 마다 서버와 통신하여 값을 저장한다
(input태그여서 evt.target에서 name을 뺄 수 있음
/ div였으면 e.target.attributes에서 빼야함)
(memberAPI.js)
... const updateMember = async (id, object) => { try { const { data } = await axios.put(`${url}/${id}`,object); console.log('[SUCCESS] UPDATE MEMBER', data); return data.data; } catch (e) { console.error('[FAIL] UPDATE MEMBER', e); throw(e); } } export { getMembers, getMember, createMember, updateMember, };
[ 결과물 ]
: 카드 hover했을 때 뜨는 휴지통 모양 클릭시 Card삭제하는 기능!
(Card.js)
import './Card.scss'; import { DeleteOutlined, FileImageOutlined } from '@ant-design/icons'; import {withRouter} from 'react-router-dom'; import {removeMember} from '../../lib/api/memberAPI'; function Card({ history, match, memberData, onRemoveCard }) { const onClickRemove = async (evt) => { /* 버블링을 막아서 클릭 이벤트 전파를 막아야 상위 요소 클릭이 눌리지 않는다 */ evt.stopPropagation(); try{ await removeMember(memberData.id); onRemoveCard(memberData.id); }catch(e){ // 오류 처리 console.log(e); } } return ( <div className="card" onClick={() => history.push(`${match.path}/${memberData.id}`)} draggable > <div className="remove-button" onClick={onClickRemove}> <DeleteOutlined style={{ fontSize: "16px"}}/> </div> <div className="image-area"> { memberData.profileUrl !== '' ? <img src={memberData.profileUrl} alt="profile"></img> : <FileImageOutlined style={{fontSize: "40px"}}/> } </div> <div className="card__content card__text name">{memberData.name}</div> <div className="card__content card__text instagram">{memberData.instagram}</div> <div className="card__content card__text introduction">{memberData.introduction}</div> { memberData.mbti && <span className="card__content card__text mbti">{memberData.mbti}</span> } </div> ); } export default withRouter(Card);
: remove-button클릭시 onClickRemove라는 이벤트 처리함수를 작성
const onClickRemove = async (evt) => { /* 버블링을 막아서 클릭 이벤트 전파를 막아야 상위 요소 클릭이 눌리지 않는다 */ evt.stopPropagation(); try{ /* 해당 id를 가진 데이터를 삭제하기 위해 removeMember에 전달 */ await removeMember(memberData.id); /* MemberList에서 받은 onRemoveCard로 전체 데이터에서 삭제해줘야 함 */ onRemoveCard(memberData.id); }catch(e){ // 오류 처리 console.log(e); } }
: 주목할 점은 바로 삭제 api를 통해 삭제 하면 서버에는 저장되지만 다시 값을 받아오지
않는 이상 우리의 메인 MemberList는 알지 못하다는 것이다.
따라서 MemberList에서 onRemoveCard라는 함수를 넘겨서 해당 처리를 해주어야 한다!
(MemberList.js)
... const removeCard = (id) => { setMembersState({ members: membersState.members.filter(mem => mem.id !== id), status: 'resolved' }); }; ... <div className="member-list-content-wrapper"> {membersState.members.map((member, i) => <Card key={"card-" + i} memberData={member} onRemoveCard={removeCard} />)} <CardEmpty onClick={onCreateCard}> <CardEmptyText>+ New</CardEmptyText> </CardEmpty> </div>
: 삭제한 id를 받아서 현재 members에서 filter로 삭제해주어야
Front에서도 다시 렌더링하여 삭제 처리를 바로 확인할 수 있다
(memberAPI.js)
... const removeMember = async(id) => { try { const { data } = await axios.delete(`${url}/${id}`); console.log('[SUCCESS] DELETE MEMBER', data); return data.data; } catch (e) { console.error('[FAIL] DELETE MEMBER', e); throw(e); } } export { getMembers, getMember, createMember, removeMember, updateMember, };
[ 결과물 ]