<리덕스 로그인 로그아웃과 토스트 - index.js>
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import ReduxApp from "./ReduxApp";
import { Provider } from "react-redux";
import { legacy_createStore } from "redux";
import rootReducer from './redux/rootReducer'
import AuthLogic from "./service/authLogic";
import { setAuth } from "./redux/userAuth/action";
import firebaseApp from "./service/firebase";
import { BrowserRouter } from "react-router-dom";
// 리덕스 적용하기
const store = legacy_createStore(rootReducer)
// AuthLogic 객체 생성하기
const authLogic = new AuthLogic(firebaseApp)
// store에있는 초기 상태 정보 출력하기
store.dispatch(setAuth(authLogic.getUserAuth(), authLogic.getGoogleAuthProvider()))
console.log(store.getState()) // getState() state.js에 있는 정보 출력
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<>
<BrowserRouter>
<Provider store={store} >
<ReduxApp />
</Provider>
</BrowserRouter>
</>
);
<리덕스 로그인 로그아웃과 토스트 - rootReducer.js>
import { combineReducers } from "redux"
import userAuth from "./userAuth/reducer";
import toastStatus from "./toastStatus/reducer";
const rootReducer = combineReducers({
userAuth, // 인증처리 관련 props 이슈
toastStatus, // 메시징처리 방법 - 소개
})
export default rootReducer;
<리덕스 로그인 로그아웃과 토스트 - ReduxApp.jsx>
import React from 'react'
import './App.css'
import { Route, Routes } from 'react-router'
import HomePage from './components/page/HomePage'
const ReduxApp = () => {
return (
<>
<Routes>
<Route path='/' exact={true} element={<HomePage />} />
</Routes>
</>
)
}
export default ReduxApp
<리덕스 로그인 로그아웃과 토스트 - ReduxHeader.jsx>
import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { loginGoogle, logout } from '../../service/authLogic'
import { useNavigate } from 'react-router'
const ReduxHeader = () => {
const navigate = useNavigate()
const {userAuth} = useSelector((store) => store)
const [userId, setUserId] = useState()
useEffect(() => {
setUserId(window.localStorage.getItem("userId"))
console.log(userId)
}, [setUserId])
const handleGoogle = async() => {
console.log('구글 로그인')
const result = await loginGoogle(userAuth.auth, userAuth.googleProvider);
console.log(result)
console.log(result.uid)
if(result.uid) {
// 로컬 스토리지나 세션 스토리지에 처리된 결과가 화면에 반영되려면 페이지 리로딩이 필요함
// navigate훅으로 처리 안되기에 reload로 처리 -> 혹은 useState훅을 사용
window.localStorage.setItem("userId", result.uid)
window.location.reload()
}
}
const handleLogout = () => {
console.log('로그아웃')
logout(userAuth.auth)
window.localStorage.removeItem("userId")
window.location.reload()
}
return (
<>
<div className='sub_container'>
<h2>헤더섹션</h2>
{userId ?
<button onClick={handleLogout}>Logout</button> :
<button onClick={handleGoogle}>Google</button>
}
</div>
</>
)
}
export default ReduxHeader
<리덕스 로그인 로그아웃과 토스트 - authLogic.js>
import { createUserWithEmailAndPassword, EmailAuthProvider, getAuth, GoogleAuthProvider, sendEmailVerification, sendPasswordResetEmail, signInWithEmailAndPassword, signInWithPopup } from 'firebase/auth'
class AuthLogic { // 클래스 선언
// 생성자 - 전역변수 초기화
constructor() {
this.auth = getAuth() // firebase developer console에서 신청한 프로젝트 설정정보 확인
this.googleProvider = new GoogleAuthProvider() // 구글인증, 카카오인증, 깃허브인증 등 구분
// this.kakaoProvider = new KakaoAuthProvider()
// this.githubProvider = new GithubAuthProvider()
}
// auth를 반환하는 함수
getUserAuth = () => {
return this.auth
}
// googleProvider를 반환하는 함수
getGoogleAuthProvider = () => {
return this.googleProvider
}
}
export default AuthLogic // 외부 js에서 사용할 때
// 사용자가 변경되는지 지속적을 체크하영 변경될때마다 호출됨 - 구글서버에서 제공하는 서비스
// 콜백함수
export const onAuthChange = (auth) => {
return new Promise((resolve) => { // 비동기 서비스 구현
// 사용자가 바뀌었을 때 콜백함수를 받아서
auth.onAuthStateChanged((user) => { // 파라미터 주입
resolve(user) // 내보내지는 정보 - View계층 - App.jsx
});
}) // end of Promise
} // end of onAuthChange
// 로그아웃 버튼 클릭시 호출하기
export const logout = (auth) => {
return new Promise((resolve, reject) => {
auth.signOut().catch(e => reject(e + '로그아웃 오류입니다.'))
// 우리회사가 제공하는 서비스를 누리기 위해서는 구글에서 제공하는 기본 정보 외에
// 추가로 필요한 정보가 있다 - 테이블설계에 반영 - 세션에 담음
// 로그인 성공시 세션 스토리지에 담아둔 정보를 모두 지운다
window.sessionStorage.clear();
// 서비스를 더 이상 사용하지 않는 경우이므로 돌려줄 값은 없다
resolve(); // 그러므로 파라미터를 비움
})
} // end of logout
// 이메일과 비밀번호로 회원가입 신청을 한 경우 로그인 처리하는 함수
// auth - AuthLogic클래스 생성자에서 getAuth()로 받아오는 전역변수 auth
// user - email, password
export const loginEmail = (auth, user) => {
console.log(auth)
console.log(user.email + user.password)
return new Promise((resolve, reject) => {
signInWithEmailAndPassword(auth, user.email, user.password)
.then((userCredential) => {
// Signed in
const user = userCredential.user;
console.log(user)
resolve(userCredential)
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
console.log(errorCode + ", " + errorMessage)
reject(error)
});
})
}
// 로그인 시도시 구글인증인지 아니면 깃허브 인증인지 문자열로 넘겨받음
// 구글 인증인 경우 - Google
// 깃허브 인증인 경우 - Github
export const loginGoogle = (auth, googleProvider) => {
return new Promise((resolve, reject) => {
signInWithPopup(auth, googleProvider) // 팝업 열림
.then((result) => { // 콜백이 진행됨
const user = result.user; // 구글에 등록되어있는 profile정보가 담겨있음
console.log(user);
resolve(user) // 인증된 사용자 프로필 정보도 화면쪽으로 내보낸다
})
.catch(e => reject(e))
})
}
export const signupEmail = (auth, user) => {
return new Promise((resolve, reject) => {
createUserWithEmailAndPassword(auth, user.email, user.password)
.then((userCredential) => {
sendEmail(userCredential.user).then(() => {
resolve(userCredential.user.uid);
});
})
.catch((e) => reject(e));
});
};
export const linkEmail = (auth, user) => {
console.log(auth);
console.log(auth.currentUser);
console.log(user);
return new Promise((resolve, reject) => {
console.log(user.email + "," + user.password);
const credential = EmailAuthProvider.credential(user.email, user.password);
console.log(credential);
console.log(auth.currentUser.uid);
resolve(auth.currentUser.uid)
/* 인증정보가 다른 사용자 계정에 이미 연결되어 있다면 아래 코드 에러 발생함
linkWithCredential(auth.currentUser, credential)
.then((usercred) => {
console.log(usercred);
const user = usercred.user;
console.log("Account linking success", user.uid);
resolve(user.uid);
})
.catch((e) => reject(e));
*/
});
};
export const sendEmail = (user) => {
return new Promise((resolve, reject) => {
sendEmailVerification(user)
.then(() => {
resolve("해당 이메일에서 인증메세지를 확인 후 다시 로그인 해주세요.");
})
.catch((e) => reject(e + ": 인증메일 오류입니다."));
});
};
export const sendResetpwEmail = (auth, email) => {
console.log(email)
return new Promise((resolve, reject) => {
sendPasswordResetEmail(auth, email)
.then(()=> {
resolve('비밀번호 변경이메일을 전송했습니다.')
})
.catch((e) => reject(e))
})
}
<리덕스 로그인 로그아웃과 토스트 - HomePage.jsx>
import React, { useEffect } from 'react'
import ReduxHeader from '../include/ReduxHeader'
import ReduxFooter from '../include/ReduxFooter'
import { useDispatch, useSelector } from 'react-redux'
import Toast from '../Toast'
import { setToastMsg } from '../../redux/toastStatus/action'
const HomePage = () => {
const status = useSelector(store => store.toastStatus.status)
console.log(status)
const dispatch = useDispatch()
useEffect (() => {
const userId = localStorage.getItem('userId')
console.log(userId)
if(userId !== null && userId.length > 0) {
// setToastMsg가 호출되면 false가 true로 변경됨
dispatch(setToastMsg('로그인 되었습니다.'))
} else {
dispatch(setToastMsg('로그인이 필요합니다.'))
}
}, [])
return (
<>
<ReduxHeader />
<div className='container'>
{status && <Toast />}
Home 페이지
</div>
<ReduxFooter />
</>
)
}
export default HomePage
<리덕스 로그인 로그아웃과 토스트 - Toast.jsx>
import React, { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { setToastFalse } from '../redux/toastStatus/action';
import './toast.css';
const ToastDiv = styled.div`
position: fixed;
top: 50%;
left: 50%;
padding: 11px;
min-width: 350px;
transform: translate(-50%, -50%);
justify-content: center;
text-align: center;
//font-weight: bold;
font-size: 18px;
z-index: 99;
background: rgba(0, 0, 0, 0.7);
color: #ffffff;
border-radius: 4px;
border: 1px solid #000000;
`
const Toast = () => {
const toastStatus = useSelector(state => state.toastStatus);
const dispatch = useDispatch();
useEffect(() => {
if (toastStatus.status) {
setTimeout(() => {
dispatch(setToastFalse());
}, 1500)
}
}, [toastStatus.status, dispatch]);
return (
<ToastDiv>{JSON.stringify(toastStatus.msg)}</ToastDiv>
);
};
export default Toast;
<리덕스 로그인 로그아웃과 토스트 - state.js>
// reducer에서 변경하는 data에대한 선언 및 초기화
export const toastStatus = {
status: false,
msg: "",
}
<리덕스 로그인 로그아웃과 토스트 - reducer.js>
import { toastStatus } from "./state";
import { SET_FALSE, SET_MSG } from "./action";
// 아래 toastInfo함수이름을 직접 사용하지 않음
export default function toastInfo(state = toastStatus, action) {
switch(action.type) {
case SET_MSG:
return {
...state, status: action.bool, msg: action.msg
}
case SET_FALSE:
return {
...state, status: action.bool, msg: action.msg
}
default:
return { ...state }
}
} // end of toastInfo
<리덕스 로그인 로그아웃과 토스트 - action.js>
// action에서 사용되는 타입 선언
export const SET_MSG = 'TOAST_STATUS/SET_MSG'
export const SET_FALSE = 'TOAST_STATUS/SET_FALSE'
// Action을 dispatch를 통해서 store에 전달할 때 호출되는 함수
// 이것이 리듀서에 전달되면 switch문에서 변화
export const setToastMsg = (msg) => {
return {
type: SET_MSG,
msg: msg,
bool: true,
}
}
export const setToastFalse = () => {
return {
type: SET_FALSE,
msg: '',
bool: false,
}
}
<유투브 클론코딩 step1 - App.jsx>
import { useEffect, useState } from "react";
import "./App.css";
import axios from "axios";
import VideoList from "./components/VideoList";
const App = () => {
console.log('App')
const [videos, setVideos] = useState([])
const [params, setParams] = useState({
part: 'snippet',
chart: 'mostPopular',
maxResults: 25,
key: ''
})
useEffect(() => {
console.log('Effect')
axios.get('<https://youtube.googleapis.com/youtube/v3/videos?'>, {params})
.then(result => {
console.log(result.data.items)
setVideos(result.data.items)
}).catch(error => console.log(error))
},[]) // 상태훅이 변경되면 그때마다 자동호출
console.log(videos)
return (
<>
)}
export default App
<유투브 클론코딩 step1 - VideoList.jsx>
import React from 'react'
import VideoItem from './VideoItem'
import styled from 'styled-components'
const UL = styled.ul`
display: flex;
flex-wrap: wrap;
list-style: none;
padding-left: 0;
margin: 0;
`
const VideoList = ({videos}) => {
console.log(videos)
return (
<UL>
{
videos.map(video => (
<VideoItem key={video.id} video={video} />
))
}
</UL>
)
}
export default VideoList
<유투브 클론코딩 step1 - VideoItem.jsx>
import React from 'react'
import styled from 'styled-components'
const LI = styled.li`
width: 50%;
padding: 0.2em;
`
const VIDEODIV = styled.div`
width: 100%;
height: 100%;
display: flex; /* 비디오가 한 줄에 나오게 함 */
align-items: center;
border: 1px solid lightgray;
box-shadow: 3px 3px 5px 0px rgba(191, 191, 191, 0.55); /* 그림자효과 */
cursor: pointer;
transition: transform 250ms easy-in; /* 자연스럽게 처리 */
&:hover {
transform: scale(1.02);
}
`
const IMG = styled.img`
width: 40%;
height: 100%;
`
const DIV = styled.div`
margin-left: 0.3em;
`
const PTITLE = styled.p`
margin: 10;
font-size: 0.8rem;
`
const PCHANNEL = styled.p`
margin: 0;
font-size: 0.6rem;
`
const VideoItem = ({video}) => {
return (
<LI>
<VIDEODIV>
<IMG src={video.snippet.thumbnails.medium.url} alt="video" />
<DIV>
<PTITLE>{video.snippet.title}</PTITLE>
<PCHANNEL>{video.snippet.channelTitle}</PCHANNEL>
</DIV>
</VIDEODIV>
</LI>
)
}
export default VideoItem
<유투브 클론코딩 step2 - App.jsx>
import "./App.css";
import YoutubePage from "./components/page/YoutubePage";
const App = () => {
console.log('App')
return (
<>
<YoutubePage />
</>
)}
export default App
<유투브 클론코딩 step2 - YoutubePage.jsx>
import axios from 'axios'
import React, { useEffect, useState } from 'react'
import VideoList from '../VideoList'
import { Button, Form, InputGroup } from 'react-bootstrap'
import VideoDetail from '../VideoDetail'
/*
기능 추가 - 사용자가 입력한 키워드 관리
1. 검색기 추가
2. 비디오 선택시 상세페이지
*/
const YoutubePage = () => {
// 사용자가 입력한 키워드 관리
const [keyword, setKeyword] = useState()
// 상세화면 추가로 인한 훅 필요
const [selectedVideo, setSelectedVideo] = useState(null)
const [videos, setVideos] = useState([])
// 비디오가 선택되면 상태값을 관리함
const videoSelect = (video) => {
console.log(videoSelect)
setSelectedVideo(video)
}
const [params, setParams] = useState({
part: 'snippet',
chart: 'mostPopular',
maxResults: 25,
key: 'AIzaSyDjYtg176tweVRBmqZO-2e9J1WbIcPsAhA'
})
useEffect(() => {
console.log('Effect')
axios.get('https://youtube.googleapis.com/youtube/v3/videos?', {params})
.then(result => {
console.log(result.data.items)
setVideos(result.data.items)
}).catch(error => console.log(error))
},[]) // 상태훅이 변경되면 그때마다 자동호출
// 대문자로 적으면 컴포넌트 취급받으니 주의!
const youtubeSearch = () => {
axios.get(`https://youtube.googleapis.com/youtube/v3/search?part=snippet&maxResults=25&q=${keyword}&type=video&key=AIzaSyDjYtg176tweVRBmqZO-2e9J1WbIcPsAhA`)
.then(result => {
console.log(result.data.items)
setVideos(result.data.items)
setSelectedVideo(null)
}).catch(error => console.log(error))
}
// 사용자가 입력한 키워드 넣기
const changeKeyword = (event) => {
console.log(event.target.value)
setKeyword(event.target.value)
}
return (
<>
<div className='container'>
<div>
<h2>Youtube<small>유투브</small></h2>
<hr />
</div>
<InputGroup className="mb-3">
<Form.Control
placeholder="검색어"
aria-label="검색어"
aria-describedby="basic-addon2"
onChange={changeKeyword}
/>
<Button className='btn btn-danger' onClick={youtubeSearch}>Search</Button>
</InputGroup>
{
selectedVideo && (
<div>
<VideoDetail video={selectedVideo} />
</div>
)
}
<VideoList videos={videos} videoSelect={videoSelect} />
</div>
</>
)
}
export default YoutubePage
/*
리덕스 설정 - index.js에서 처리한다
-> Provider store설정 - reducer읽기 - dispatch - state초기화 - 변수들에대한 목록, 초기값 확인
-> state값은 언제 바뀌는가? dispatch(setToastMsg('메시지'))하면 false -> true, action전달 1)action.type 2)payload
App.jsx에서는 라우트 처리를 한다 - 화면전환
*/
<유투브 클론코딩 step2 - VideoList.jsx>
import React from 'react'
import VideoItem from './VideoItem'
import styled from 'styled-components'
const VideoListDiv = styled.div`
display: grid;
margin-top: 10px;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
`
const VideoList = ({videos, videoSelect}) => {
console.log(videos)
return (
<>
<VideoListDiv>
{
videos.map((video, index) => (
<VideoItem key={index} video={video} videoSelect={videoSelect} />
))
}
</VideoListDiv>
</>
)
}
export default VideoList
<유투브 클론코딩 step2 - VideoItem.jsx>
import React from 'react'
import styled from 'styled-components'
import VideoDetail from './VideoDetail'
const VideoLi = styled.li`
padding: 0.2em;
list-style: none;
`
const VideoCard = styled.div`
width: 100%;
height: 100%;
display: flex; /* 비디오가 한 줄에 나오게 함 */
align-items: center;
border: 1px solid lightgray;
box-shadow: 3px 3px 5px 0px rgba(191, 191, 191, 0.55); /* 그림자효과 */
cursor: pointer;
transition: transform 250ms easy-in; /* 자연스럽게 처리 */
&:hover {
transform: scale(1.02);
}
`
const VideoThumbnail = styled.img`
width: 40%;
height: 100%;
`
const VideoInfo = styled.div`
margin-left: 0.3em;
`
const Ptitle = styled.p`
margin: 10;
font-size: 0.8rem;
`
const Pchannel = styled.p`
margin: 0;
font-size: 0.6rem;
`
const VideoItem = (props) => {
// 첫번째 파라미터는 비디오 한 건에 대한 정보
// 두번째 파라미터는 선택된 비디오의 이벤트처리 함수의 주소번지를 받아서
// VideoLi가 클릭되었을 때 파라미터로 video 한 건의 주소번지를 담아서
// 부모에서 정의된 이벤트 처리 함수를 호출한다
// VideoList에서 이벤트 처리를 마무리하지 않고 props로 넘기는 이유?
// -> VideoList에서는 n건을 가지고있고 이 중에서 어떤 비디오 클립이 선택되었는지 알 수 없으니까
// 이벤트 소스 클립은 리스트에 있지만 선택된 비디오 한 건에 대한 정보는 VideoItem에서 결정된다
// 그러니까 비디오 한 건에 대한 정확한 정보를 알고있는 자손 컴포넌트인 VideoItem에서
// 부모가 가진 함수의 주소번지를 props로 받고 이벤트 호출은 VideoItem에서 처리해야한다
const {video, videoSelect} = props
return (
<VideoLi onClick={() => videoSelect(video)}>
<VideoCard>
<VideoThumbnail src={video.snippet.thumbnails.medium.url} alt="video thumbnail" />
<VideoInfo>
<Ptitle>{video.snippet.title}</Ptitle>
<Pchannel>{video.snippet.channelTitle}</Pchannel>
</VideoInfo>
</VideoCard>
</VideoLi>
)
}
export default VideoItem
<유투브 클론코딩 step2 - VideoDetail.jsx>
import React from 'react'
const VideoDetail = ({video}) => {
console.log(video)
return (
<>
<section>
<iframe
title="video play"
type="text/html"
width="100%"
height="500"
src={`http://www.youtube.com/embed/${video.id.videoId}`}
frameborder="0" allowFullScreen></iframe>
</section>
</>
)
}
export default VideoDetail
'국비학원 > 수업기록' 카테고리의 다른 글
국비 지원 개발자 과정_Day92 (0) | 2023.04.11 |
---|---|
국비 지원 개발자 과정_Day91 (0) | 2023.04.07 |
국비 지원 개발자 과정_Day89 (0) | 2023.04.05 |
국비 지원 개발자 과정_Day88 (0) | 2023.04.04 |
국비 지원 개발자 과정_Day87 (0) | 2023.04.03 |
댓글