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

2023. 2. 7. 13:36·Web/인스타 클론 코딩
목차
  1. 1. ProfileContainer 작성
  2. 2. 프로필 수정 부분 작성
  3. 3. 게시글 목록 확인 부분 작성
  4. 4. 동작 모습

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

 

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

    • 홈
  • 링크

    • GITHUB
  • 공지사항

  • 인기 글

  • 태그

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

  • 최근 글

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

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.