카테고리 없음
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; // 다 본거 같네 휴우 흐음.... 재밌었다. 파이어베이스 아직 넘후 어렵다.