23/12/18 심화팀과제 review(4)

2023. 12. 19. 09:02카테고리 없음

회원가입 페이지를 봅세다.

 

<Register.jsx>

import React, {useState} from 'react';
import Button from '../components/UI/Button';
import {createUserWithEmailAndPassword, updateProfile} from 'firebase/auth';
import {auth} from '../shared/firebase';
import styled from 'styled-components';
import {useNavigate} from 'react-router-dom';
import {toast} from 'react-toastify';

const Register = () => {
  const navigate = useNavigate();
  const [inputs, setInputs] = useState({  //깔끔하게 객체형으로 받으시는구먼
    email: '',
    nickname: '',
    password: '',
  });

  // input 변경
  const changeInputs = e => {     //이거 로그인 페이지에서도 봤던건데 파일하나에 넣어놓으시고
    setInputs({                               //익스포트해서 임포트 받아 쓰시면 더 낫지 않을깝숑?
      ...inputs,
      [e.target.name]: e.target.value,
    });
  };

  // input 비우기
  const clearInputs = () => {            // 아하 여기에는 닉네임 키값이 하나 더 있어서 
    setInputs({                          //공용함수화 하기 귀찮으셨나? nickname은 그냥 설정만 하시고
      email: '',                         //안쓰셔도 됐을텐데 흐음... 타입스크립트였으면 nickname? 이렇게 하면
      nickname: '',                      //되나? 타입스크립트 공부좀 해야겠구먼
      password: '',
    });
  };

  // 유효성 검사
  const checkInputs = () => {            
    // 빈칸
    if (inputs.email.trim().length === 0 || inputs.email.trim().length === 0 || inputs.nickname.trim().length === 0) {
      toast.error('정보를 모두 입력해주세요', {                    //trim()은 양쪽의 공백을 지우는 함수라고 함
        position: 'top-center',
        autoClose: 3000,
        hideProgressBar: false,
        closeOnClick: true,
        pauseOnHover: true,
        draggable: false,
        progress: undefined,
        theme: 'colored',
      });
      clearInputs();                   // 인풋 지워주고 
      return;                            // 인풋들에 값들이 없으면 함수 종료해주고
    }
    // 닉네임 2~6자 사이
    if (inputs.nickname.length < 2 || inputs.nickname.length > 6) {     //닉네임 갯수에 문제가 있으면
      toast.error('닉네임은 2~6자 사이로 만들어주세요', {
        position: 'top-center',
        autoClose: 3000,
        hideProgressBar: false,
        closeOnClick: true,
        pauseOnHover: true,
        draggable: false,
        progress: undefined,
        theme: 'colored',
      });
      clearInputs();
      return;                        //그때도 밑에 내용 실행 안해주고
    }
    return true;       //여기까지와야 트루를 반화하는구먼
  };

  // 오류메시지
  const errorMsg = code => {             // 오우 여기서 스위치문이라 
    switch (code) {
      case 'auth/user-not-found' || 'auth/wrong-password':      //섬세한 하드코딩 이시군
        return '이메일 혹은 비밀번호가 일치하지 않습니다.';      // 이러면 휴먼에러의 가능성이 큰데
      case 'auth/email-already-in-use':
        return '이미 사용 중인 이메일입니다.';
      case 'auth/weak-password':
        return '비밀번호는 6글자 이상이어야 합니다.';
      case 'auth/network-request-failed':
        return '네트워크 연결에 실패 하였습니다.';
      case 'auth/invalid-email':
        return '잘못된 이메일 형식입니다.';
      case 'auth/internal-error':
        return '잘못된 요청입니다.';
      default:
        return '로그인에 실패 하였습니다.';
    }
  };

  // 회원가입
  const registerUser = async e => {  //async가 있는거 봐선 비동기 통신 함수가 밑에 있겠구먼
    e.preventDefault();    폼태그니까 새로고침 막고

    if (!checkInputs()) {
      return;                     //위에 체크 인풋 함수 통과 못하면 리턴
    }

    const defaultPhotoUrl =
    try {
      const userCredential = await createUserWithEmailAndPassword(auth, inputs.email, inputs.password);
      await updateProfile(userCredential.user, {displayName: inputs.nickname, photoURL: defaultPhotoUrl});
                                                                           //요게 파이어베이스 회원가입 함수 로직
      toast.success('회원가입 성공!', {                // createUserWithEmailAndPassword 요기에서 닉네임을
        position: 'top-center',                        //추가한게 아니라 위에 await에서는 인풋에 이메일과 패스워드만
        autoClose: 3000,              //쓰시고 바로 업데이트 하셔서 nickname과 기본사진을 추가하셨구나.
        hideProgressBar: false,     // createUserWithEmailAndPassword에  다 있지 않나? 이러면 두번통신해야
        closeOnClick: true,                  //될텐데? nickname은 내기억에 있었는데 
        pauseOnHover: true,
        draggable: false,
        progress: undefined,
        theme: 'colored',
      });
      navigate('/login');
    } catch (error) {
      toast.error(errorMsg(error.code), {
        position: 'top-center',
        autoClose: 3000,
        hideProgressBar: false,
        closeOnClick: true,
        pauseOnHover: true,
        draggable: false,
        progress: undefined,
        theme: 'colored',
      });
    }

    setInputs({         //인풋창 비워주고
      email: '',
      nickname: '',
      password: '',
    });
  };

  const moveToLoginPage = e => {
    e.preventDefault();
    navigate('/login');           // 로그인페이지로 옮겨주는 로직
  };

  return (
    <ScWrapper>
      <ScForm>
        <h1>회원가입</h1>
        <input type="email" placeholder="이메일" name="email" value={inputs.email} onChange={changeInputs} />
        <input type="text" placeholder="닉네임" name="nickname" value={inputs.nickname} onChange={changeInputs} />
        <input type="password" placeholder="비밀번호" name="password" value={inputs.password} onChange={changeInputs} />
        <Button onClick={registerUser}>회원가입</Button>
        <span onClick={moveToLoginPage}>로그인 하러가기</span>
      </ScForm>
    </ScWrapper>
  );
};

const ScWrapper = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100vh;
  background-color: var(--light-blue);
`;

const ScForm = styled.form`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 40%;
  height: 50%;
  padding: 30px;
  border-radius: 10px;
  background-color: var(--white);

  h1 {
    font-size: 2rem;
    font-weight: bold;
    margin-bottom: 30px;
  }

  input {
    width: 50%;
    border: none;
    padding: 10px;
    border-radius: 10px;
    margin-bottom: 10px;
    background-color: #eee;
  }

  input:nth-child(4) {
    margin-bottom: 20px;
  }

  button {
    width: 50%;
    margin-bottom: 10px;
  }

  span {
    margin-top: 10px;
    cursor: pointer;
    color: var(--deep-blue);
    font-weight: bold;
  }
`;

export default Register;                   // 회원가입이라 딱히 RTK는 안쓰셨구먼 ㅇㅋㅇㅋ

 

<ProfilePage.jsx> 프로필페이지를 봅세다. 

여기서는 회원가입한 저 유저의 개인정보를 변경하고 카카오맵을 끌어다 쓰셨다.

import React, {useEffect, useRef, useState} from 'react';    //useRef 써본적이 나는 없다.
import styled from 'styled-components';
import Button from '../components/UI/Button';
import {useDispatch, useSelector} from 'react-redux';
import {getAuth, updateProfile} from '@firebase/auth';    //getAuth()로 바로 인증정보가져다 쓰셨구먼
import {updateNickname, updatePhoto} from '../redux/modules/Auth';    //요거 리듀서네 이따 봅세다. 
import {getDownloadURL, ref, uploadBytes} from 'firebase/storage';   //스토리지 오호
import {db, storage} from '../shared/firebase';
import MapComponent from '../components/MapComponent';          //맵용 컴포넌틑가 따로 있으시군
import {collection, getDocs, setDoc, doc} from '@firebase/firestore';
import {setList} from '../redux/modules/fixList';
import {toast} from 'react-toastify';

const ProfilePage = () => {
  const auth = getAuth();
  const dispatch = useDispatch();
  const userInfo = useSelector(state => state.auth);
  const {displayName, photoURL} = userInfo;               // RTK 스테이트에서 구조분해로
  const [nickNameEditShown, setNickNameEditShown] = useState(false);   //  닉네임과 포토만 쓰시는군
  const [photoEditShown, setPhotoEditShown] = useState(false);        //이건 불리언 값인거로 봐서
  const [nickname, setNickname] = useState(displayName);             //토글인듯
  const [imgFile, setImgFile] = useState('');
  const [previewImage, setPreviewImage] = useState(photoURL);  //변경할 닉네임과 포토 담을 그릇
  const imgRef = useRef();            //이미지 Ref 오호

  const list = useSelector(state => state.fixList);    //내 리덕스에서 LIST 가져오시고

  useEffect(() => {
    dataReading();      //어 이거 내가 만든건데? 퍼오셨구먼?
  }, [dispatch]);

  useEffect(() => {
    updatePhotoAndNickname();
  }, [displayName, photoURL]);

  // 데이터 읽어오기 (파이어베이스)
  const dataReading = async () => {
    const querySnapshot = await getDocs(collection(db, 'fixs'));
    let dataArr = [];
    querySnapshot.forEach(doc => {
      const data = doc.data();
      dataArr.push({...data, id: doc.id});
      dataArr = dataArr.sort((a, b) => b.createdAt - a.createdAt);
    });                                                    // 파이어 베이스에서 데이터 가져오고 아이디 추가해주고
                                                              // 배열 시간순으로 sort 해주고 
    dispatch(setList(dataArr));                  //디스패치로 화면 리랜더링 시켜주고
  };

  const filteredList = list.filter(item => {
    return item.uid == auth.currentUser.uid;           //유아이디가 같은거만 뽑으셨고
  });                                                                       //getAuth() 로 가져온 auth의 커런트유저와 비교하셨군

  // 프로필 변경 후 바로 적용
  const updatePhotoAndNickname = () => {
    try {
      filteredList.map(async item => {
        await setDoc(doc(db, 'fixs', `${item.id}`), {    //내가 만든거랑 합치셨구만 훨씬 깔끔하군
          ...item,
          displayName,
          photoURL,
        });
      });
    } catch (error) {
      toast.error('수정에 실패했습니다', {
        position: 'top-center',
        autoClose: 3000,
        hideProgressBar: false,
        closeOnClick: true,
        pauseOnHover: true,
        draggable: false,
        progress: undefined,
        theme: 'colored',
      });
    }
  };

  // map에 정보 전달
  const coordinates = filteredList.map(item => {
    return {
      id: item.id,                      //오호 맵함수로 키값을 취향대로 바꾸시고
      title: item.title,               //isOpen 추가하셨구나
      lat: item.latitude,
      lng: item.longitude,
      isOpen: false,
      image: item.image_url,
    };
  });

  // 이미지 저장
  const saveImgFile = e => {
    setImgFile(e.target.files[0]);             // target은 인풋의 해당 태그겠고 여러개의 파일중 첫번째를 쓰는군
    setPreviewImage(URL.createObjectURL(e.target.files[0]));    //그 파일을 오브젝트 유알엘을 만드는구먼
  };                                                 

  // input 변경
  const changeNickName = e => {
    setNickname(e.target.value);
  };

  // 이미지 수정하기
  const handleUpdatePhoto = () => {
    setPhotoEditShown(true);
  };

  // 이미지 수정취소
  const cancelUpdatePhoto = () => {
    setPhotoEditShown(false);
    setImgFile('');
    setPreviewImage(photoURL);
  };

  // 이미지 수정(업로드)
  const handleEditPhoto = async () => {
    try {
      const storageRef = ref(storage, `${auth.currentUser.uid}/profile`);  //ref를 써보진 않았는데 이렇게 쓰는군
      await uploadBytes(storageRef, imgFile);                                        //스토리지에 위에 이미지팔 업로드
      const downloadURL = await getDownloadURL(storageRef);
      await updateProfile(auth.currentUser, {photoURL: downloadURL});  //포토 업데이트
      dispatch(updatePhoto(downloadURL));      //RTK적용해주고
      setPhotoEditShown(false);                     //업로드 완료되면 다시 토글해주고
      toast.success('수정되었습니다');
    } catch (error) {
      toast.error('수정에 실패했습니다', {
        position: 'top-center',
        autoClose: 3000,
        hideProgressBar: false,
        closeOnClick: true,
        pauseOnHover: true,
        draggable: false,
        progress: undefined,
        theme: 'colored',
      });
    }
  };

  // 프로필 수정하기
  const handleUpdateNickname = () => {   //프로필 수정도 사진수정과 비슷할듯
    setNickNameEditShown(true);
  };

  // 프로필 수정 취소
  const cancelUpdateNickname = () => {
    setNickNameEditShown(false);                   //역시 닉네임 미리보기 토글하시는군
    setNickname(displayName);                       //태그에서 이값에 따라 화면 보여지고 말고를 정하시겠지
  };

  // 프로필 수정
  const handleEditNickname = async () => {
    if (nickname.trim().length < 1) {
      alert('내용을 입력해주세요');
      return;
    }

    if (nickname.trim().length < 2 || nickname.trim().length > 6) {
      alert('닉네임은 2~4글자 사이로 해주세요');
      return;
    }                                                                          //여기까지는 유효성이고
    try {
      // firebase 저장
      await updateProfile(auth.currentUser, {displayName: nickname});
      setNickNameEditShown(false);
      setNickname('');
      // redux에 업데이트
      dispatch(updateNickname(nickname));    //닉네임 파이어베이스에 변경값 저장해주고
                                                                        //리덕스 스테이트 바꿔주고
      // toast.success('수정되었습니다');
    } catch (error) {
      toast.error('수정에 실패했습니다', {
        position: 'top-center',
        autoClose: 3000,
        hideProgressBar: false,
        closeOnClick: true,
        pauseOnHover: true,
        draggable: false,
        progress: undefined,
        theme: 'colored',
      });
    }
  };

  return (
    <ScMyPageWrapper>
      <section>
        <h2>마이페이지</h2>
        <ScProfileWrapper>
          <div>                                   //htmlFor로 input역할 대신이라 잘쓰시네
            <label htmlFor="profileImg" onClick={handleUpdatePhoto}>
              <input
                type="file"                             // 요게 타입이 파일이구먼
                accept="image/*"
                id="profileImg"
                style={{display: 'none'}}                  //디스플레이 논이군 
                onChange={saveImgFile}
                ref={imgRef}
              />
              <ScImgWrapper>
                <img src={previewImage} alt="" />   //미리보기 이미지라 처음에는 유저의 기본 화면이군
              </ScImgWrapper>
            </label>

            {photoEditShown ? (
              <div>
                <ScBtnWrapper>
                  <Button onClick={handleEditPhoto}>수정</Button>
                  <Button onClick={cancelUpdatePhoto}>취소</Button>
                </ScBtnWrapper>
              </div>
            ) : (
              <ScLabel onClick={handleUpdatePhoto}>프로필 이미지 변경</ScLabel>
            )}
          </div>
          <div>
            <h3>
              <span>{displayName}</span> 님 반갑습니다!
            </h3>
            {nickNameEditShown ? (
              <>
                <div>
                  <input type="text" value={nickname} onChange={changeNickName} />
                </div>
                <ScBtnWrapper>
                  <Button onClick={handleEditNickname}>수정</Button>
                  <Button onClick={cancelUpdateNickname}>취소</Button>
                </ScBtnWrapper>
              </>
            ) : (
              <Button onClick={handleUpdateNickname}>프로필 수정하기</Button>
            )}
          </div>
        </ScProfileWrapper>
      </section>
      <hr />
      <section>
        <h2>내 Fix보기</h2>
      </section>
      <MapComponent coordinates={coordinates} />
    </ScMyPageWrapper>
  );
};

const ScMyPageWrapper = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 100%;
  padding: 50px 0;
  text-align: center;

  h2 {
    font-size: 2rem;
    font-weight: bold;
    margin-bottom: 50px;
  }

  hr {
    width: 80%;
    background-color: #eee;
    margin-top: 50px;
    margin-bottom: 50px;
  }

  section {
    width: 80%;
  }

  input {
    border: 1px solid #868686;
    border-radius: 10px;
    padding: 10px;
  }
`;

const ScProfileWrapper = styled.div`
  display: flex;
  justify-content: space-evenly;
  align-items: center;

  figure {
    margin-right: 20px;
  }

  h3 {
    font-size: 1.5rem;
    margin-bottom: 30px;
  }

  h3 span {
    color: var(--deep-blue);
    font-weight: bold;
  }
`;

const ScImgWrapper = styled.figure`
  border: 1px solid black;
  border-radius: 50%;
  overflow: hidden;
  width: 200px;
  height: 200px;

  cursor: pointer;

  img {
    object-fit: cover;
    width: 100%;
    height: 100%;
  }
`;

const ScLabel = styled.label`
  display: inline-block;
  font-weight: bold;
  cursor: pointer;
  color: var(--deep-blue);
  margin-top: 20px;
`;

const ScBtnWrapper = styled.div`
  display: flex;
  justify-content: center;
  margin-top: 30px;
  gap: 10px;

  button {
    width: 30%;
  }
`;

export default ProfilePage;           // 다 본거 같네 휴우 흐음.... 재밌었다. 파이어베이스 아직 넘후 어렵다.