[인스타그램 클론코딩] 11. 프로필 수정 기능 구현(Front-End)

2023. 2. 7. 13:36·Web/인스타 클론 코딩

프로필 탭은 본인의 프로필 정보를 확인할 수 있는 부분과, 본인이 게시한 게시글들을 볼 수 있는 부분으로 나누었다.

 

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
'Web/인스타 클론 코딩' 카테고리의 다른 글
  • [인스타그램 클론코딩] 12. 메신저 기능 구현(Front-End)
  • [인스타그램 클론코딩] 12. 메신저 기능 구현(Back-End)
  • [인스타그램 클론코딩] 11. 프로필 수정 기능 구현(Back-End)
  • [인스타그램 클론코딩] 10. 게시물 댓글 기능 구현(Front-End)
뚝딱뚝딱2
뚝딱뚝딱2
  • 뚝딱뚝딱2
    개발도상국
    뚝딱뚝딱2
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 공부
        • Java
        • Spring Boot
        • LORA
      • Web
        • 인스타 클론 코딩
        • GPT 응답 API 서버
        • Spring Boot 예외 처리
        • 코테 준비용 서비스 만들기
      • DevOps
        • 쿠버네티스
        • 서버 만들기
      • 코딩테스트
        • 알고리즘
      • 교육
        • 스파르타코딩클럽 - 내일배움단
        • 혼자 공부하는 컴퓨터 구조 운영체제
      • 잡다한것
  • 블로그 메뉴

    • 홈
  • 링크

    • GITHUB
  • 공지사항

  • 인기 글

  • 태그

    오블완
    쿠버네티스
    spring boot
    스프링부트
    클론코딩
    Java
    인스타그램
    react
    MSA
    예외
    mapstruct
    REST API
    스프링 부트
    리액트
    클러스터
    chat GPT
    Entity
    백준
    티스토리챌린지
    OpenAI API
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
뚝딱뚝딱2
[인스타그램 클론코딩] 11. 프로필 수정 기능 구현(Front-End)
상단으로

티스토리툴바