프로필 탭은 본인의 프로필 정보를 확인할 수 있는 부분과, 본인이 게시한 게시글들을 볼 수 있는 부분으로 나누었다.
1. ProfileContainer 작성
const ProfileContainer = () => {
const userDetail = useSelector(state => state.userDetail);
const navigate = useNavigate();
const [profileCount, setProfileCount] = useState(
{board: 0, follower: 0, follow: 0}
);
const [postList, setPostList] = useState();
const [postIsOpen, setPostIsOpen] = useState();
const getProfileCount = () => {
withJwtAxios.get("/profiles/count")
.then((res) => {
setProfileCount(res.data);
})
}
const getPostList = () => {
withJwtAxios.get("/post-my-list")
.then((res) => {
setPostList(res.data.postList);
setPostIsOpen(new Array(res.data.postList.length).fill(false));
})
}
const deletePost = (postId) => {
if(window.confirm("삭제 하시겠습니까?") === true) {
withJwtAxios.delete("/post", {params: {postId: postId}})
.then((res) => {
setPostList(res.data.postList);
})
}
}
useEffect(() => {
getProfileCount();
getPostList();
}, [])
return (
<>
<div className={style.profile_container}>
<img className={style.profile_thumbnail} src={Static_Base_Url + userDetail.profileUrl}/>
<div>
<div className={style.profile_container_head}>
<div className={style.nickname}>
{userDetail.nickname}
</div>
<button className='btn btn-outline-dark btn-sm' style={{fontWeight: 'bold'}} onClick={() => navigate('/outstagram/profile-edit/edit')}>프로필 수정</button>
</div>
<div className={style.profile_count}>
<div className={style.count}>게시물 {profileCount.board}</div>
<div className={style.count}>팔로워 {profileCount.follower}</div>
<div className={style.count}>팔로우 {profileCount.follow}</div>
</div>
</div>
</div>
<div className={style.post_container}>
<Post postList={postList} setPostList={setPostList} postIsOpen={postIsOpen} setPostIsOpen={setPostIsOpen} deletePost={deletePost}/>
</div>
</>
);
};
export default ProfileContainer;
- getProfileContm getPostList는 각각 유저의 게시글 리스트와 게시글/팔로워/팔로잉 수를 가져오는 메서드이다.
- deletePost는 해당 게시글을 삭제하는 메서드이다.
2. 프로필 수정 부분 작성
const ProfileEdit = () => {
const sideBar = [
{number: '0', link: '/outstagram/profile-edit/edit', text: '프로필 편집'},
{number: '1', link: '/outstagram/profile-edit/password', text: '비밀번호 변경'}
]
const [sideState, setSideState] = useState(
[true, false]
);
const onClickSide = (idx) => {
const temp = new Array(sideState.length).fill(false);
temp[idx] = true;
setSideState(temp);
}
return (
<div className={style.profile_modify_container}>
<div className={style.side_bar}>
{sideBar.map((side) => {
return(
<Link className={style.link} key={side.number} to={side.link} onClick={() => onClickSide(side.number)} style={sideState[side.number] ? {borderLeft:'2px solid black', fontWeight:'bolder'} : {}}>
{side.text}
</Link>
);
})}
</div>
<div className={style.main}>
<Outlet/>
</div>
</div>
);
};
const Edit = () => {
const userDetail = useSelector(state => state.userDetail);
const dispatch = useDispatch();
const navigate = useNavigate();
const [inputs, setInputs] = useState({
nickname: userDetail.nickname,
introduce: userDetail.introduce,
phone: userDetail.phone,
gender: userDetail.gender,
email: userDetail.email
});
const handleChangeThumbnail = (e) => {
const file = e.target.files[0];
if(file != null) {
const formData = new FormData();
formData.append("file", file);
multiFileAxios.post("/profiles/thumbnail", formData)
.then((res) => {
dispatch(init(res.data));
});
}
}
const onChangeInputs = (e) => {
const {value, name} = e.target;
setInputs({
...inputs,
[name]: value
});
}
const submitInputs = () => {
if(window.confirm("수정 하시겠습니까?") === true) {
withJwtAxios.post("/profiles/profile", inputs)
.then((res) => {
alert("수정이 완료되었습니다.")
dispatch(init(res.data));
navigate("/outstagram/profile")
});
}
}
return (
<>
<div className={style.thumbnail_container}>
<div className={style.thumbnail_box}>
<label htmlFor="file">
<img className={style.thumbnail} src={Static_Base_Url + userDetail.profileUrl}/>
</label>
<input type='file' id='file' accept='image/*' style={{display: 'none'}} onChange={handleChangeThumbnail}>
</input>
</div>
<div className={style.nickname_box}>
<div className={style.nickname}>
{userDetail.nickname}
</div>
<label htmlFor="file">
<div className={style.thumbnail_edit_button}>
프로필 사진 바꾸기
</div>
</label>
</div>
</div>
<div className={style.input_box}>
<label className={style.label}>닉네임</label>
<input className={style.input} name='nickname' value={inputs.nickname} onChange={onChangeInputs}/>
</div>
<div className={style.input_box}>
<label className={style.label}>소개</label>
<textarea className={style.input} name='introduce' value={inputs.introduce} onChange={onChangeInputs}/>
</div>
<div className={style.input_box}>
<label className={style.label}>이메일</label>
<input className={style.input} name='email' value={inputs.email} onChange={onChangeInputs} disabled/>
</div>
<div className={style.input_box}>
<label className={style.label}>전화번호</label>
<input className={style.input} name='phone' value={inputs.phone} onChange={onChangeInputs}/>
</div>
<div className={style.input_box}>
<label className={style.label}>성별</label>
<input className={style.input} name='gender' value={inputs.gender} onChange={onChangeInputs}/>
</div>
<button className='btn btn-primary' onClick={submitInputs}>제출</button>
</>
);
};
export default Edit;
const Password = () => {
const userDetail = useSelector(state => state.userDetail);
const navigate = useNavigate();
const [inputs, setInputs] = useState({
prevPw: '',
changePw: '',
changePwCheck: '',
email: userDetail.email
});
const handleChangeInputs = (e) => {
const {value, name} = e.target;
setInputs({
...inputs,
[name]: value
});
};
const submitPassword = () => {
withJwtAxios.post("/password", inputs)
.then((res) => {
alert(res.data);
navigate("/outstagram/profile")
})
}
return (
<>
<div className={style.input_box}>
<label className={style.label}>현재 비밀번호</label>
<input className={style.input} name='prevPw' onChange={handleChangeInputs} type='password'/>
</div>
<div className={style.input_box}>
<label className={style.label}>변경할 비밀번호</label>
<input className={style.input} name='changePw' onChange={handleChangeInputs} type='password'/>
</div>
<div className={style.input_box}>
<label className={style.label}>비밀번호 확인</label>
<input className={style.input} name='changePwCheck' onChange={handleChangeInputs} type='password'/>
</div>
<button className='btn btn-primary' onClick={submitPassword}>제출</button>
</>
);
};
export default Password;
- ProfileEdit은 프로필 수정 메뉴를 선택하여 해당 컴포넌트를 랜더링 해준다.
- Edit은 유저의 프로필 세부사항과 섬네일 사진을 수정할 수 있게해주는 컴포넌트이다.
- Password는 유저의 비밀번호를 업데이트 해주는 컴포넌트이다.
3. 게시글 목록 확인 부분 작성
const Post = (props) => {
const postList = props.postList;
const postIsOpen = props.postIsOpen;
const setPostIsOpen = props.setPostIsOpen;
const deletePost = props.deletePost;
const onClickPost = (idx) => {
const temp = JSON.parse(JSON.stringify(postIsOpen));
temp[idx] = true;
setPostIsOpen(temp);
}
const onClickExitPost = (idx) => {
const temp = JSON.parse(JSON.stringify(postIsOpen));
temp[idx] = false;
setPostIsOpen(temp);
}
const fileRender = (url) => {
const extension = url.split('/').pop().split('.').pop();
if(['mp4', 'mov', 'avi', 'wmv', 'MP4', 'MOV', 'AVI', 'WMV'].includes(extension)) {
return(
<video className={style.image} src={url} />
)
} else {
return(
<img className={style.image} src={url} />
)
}
}
return (
<>
<div className={style.header}>
<i className="bi bi-border-all fs-5"/>
<div className={style.header_title}>게시물</div>
</div>
<div>
{postList !== undefined ?
<div className={style.container}>
{postList.map((post, idx) => {
const thumbnail = Static_Base_Url + post.postFileList[0].fileUrl;
return(
<div key={idx}>
<div className={style.row} onClick={() => onClickPost(idx)}>
{fileRender(thumbnail)}
</div>
<PostDetail post={post} isOpen={postIsOpen[idx]} idx={idx} onClickExitPost={onClickExitPost} deletePost={deletePost}/>
</div>
)
})
}
</div> :
<div></div>
}
</div>
</>
);
};
export default Post;
const PostDetail = (props) => {
const isOpen = props.isOpen;
const idx = props.idx;
const onClickExitPost = props.onClickExitPost;
const post = props.post;
const postFileList = post.postFileList;
const deletePost = props.deletePost;
const ChatModalStyle = {
overlay: {
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(255, 255, 255, 0.45)",
zIndex: 10,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
},
content: {
display: "flex",
flexDirection: "column",
justifyContent: "start",
background: "white",
padding: 0,
top: '5%',
left: '10%',
width: '80%',
height: '90%',
WebkitOverflowScrolling: "touch",
borderRadius: "14px",
outline: "none",
zIndex: 10,
flexWrap: 'wrap'
},
}
const [fileIndex, setFileIndex] = useState(0);
const [chatList, setChatList] = useState();
const fileRender = (url) => {
const extension = url.split('/').pop().split('.').pop();
if(['mp4', 'mov', 'avi', 'wmv', 'MP4', 'MOV', 'AVI', 'WMV'].includes(extension)) {
return(
<video className={style.file} src={url} />
)
} else {
return(
<img className={style.file} src={url} />
)
}
}
const onClickLeft = () => {
if(fileIndex > 0) {
setFileIndex(() => {
return fileIndex-1
})
}
}
const onClickRight = () => {
if(fileIndex < postFileList.length - 1) {
setFileIndex(() => {
return fileIndex+1
})
}
}
const setChatCreateDate = (date) => {
const createDate = new Date(date)
const nowDate = new Date();
const diff = (nowDate.getTime() - createDate.getTime()) / 1000;
if (diff < 3600) {
return `${Math.floor(diff/60)} 분전`;
} else if(diff < 86400) {
return `${Math.floor(diff/3600)} 시간전`;
} else if(diff < 864000) {
return `${Math.floor(diff/86400)} 일전`;
} else {
return '오래전';
}
}
useEffect(() => {
withJwtAxios.get("/posts/chat", {params: {postId: post.postId}})
.then((res) => {
setChatList(res.data.postChatList);
})
}, [post])
return (
<ReactModal isOpen={isOpen} style={ChatModalStyle} ariaHideApp={false}>
<div className={style.head}>
<div style={{color: 'red', cursor: 'pointer', fontWeight:'bold'}} onClick={() => deletePost(post.postId)}>
게시물 삭제
</div>
<i className="bi bi-x-lg modal-exit-button" style={{cursor: 'pointer'}} onClick={() => {onClickExitPost(idx)}}/>
</div>
<div className={style.body}>
<div className={style.file_box}>
<div className={style.arrow_left} onClick={onClickLeft} style={{display: fileIndex === 0 ? 'none' : 'inherit'}}>
<i className="bi bi-arrow-left-circle"></i>
</div>
{fileRender(Static_Base_Url + postFileList[fileIndex].fileUrl)}
<div className={style.arrow_right} onClick={onClickRight} style={{display: fileIndex === postFileList.length-1 ? 'none' : 'inherit'}}>
<i className="bi bi-arrow-right-circle"></i>
</div>
</div>
<div className={style.body_content}>
<div className={style.content_box}>
<img className={style.profile_thumbnail} src={Static_Base_Url + post.user.profileUrl}/>
<div className={style.profile_nickname}>
{post.user.nickname}
</div>
<div className={style.content}>{post.content.slice(1, post.content.length-1)}</div>
</div>
<div className={style.chat_list}>
{chatList !== undefined ?
chatList.map((chat, idx) => {
return(
<div key={idx} className={style.chat_box}>
<img className={style.profile_thumbnail} src={Static_Base_Url + chat.profileUrl} />
<div className={style.profile_nickname}>
{chat.nickname}
<div className={style.chat_date}>
{setChatCreateDate(chat.createdDate)}
</div>
</div>
<div className={style.content}>
{chat.content}
</div>
</div>
)
})
: <div></div>
}
</div>
</div>
</div>
</ReactModal>
);
};
export default PostDetail;
- Post는 게시글 목록을 그리드 형태로 반환한다.
- PostDetail은 해당 게시글을 클릭햇을 때 모달창으로 그 내용을 띄워준다.
4. 동작 모습
'Web > 인스타 클론 코딩' 카테고리의 다른 글
[인스타그램 클론코딩] 12. 메신저 기능 구현(Front-End) (0) | 2023.02.16 |
---|---|
[인스타그램 클론코딩] 12. 메신저 기능 구현(Back-End) (0) | 2023.02.16 |
[인스타그램 클론코딩] 11. 프로필 수정 기능 구현(Back-End) (0) | 2023.02.07 |
[인스타그램 클론코딩] 10. 게시물 댓글 기능 구현(Front-End) (0) | 2023.02.07 |
[인스타그램 클론코딩] 10. 게시물 댓글 기능 구현(Back-End) (0) | 2023.02.07 |