본문 바로가기
국비학원/수업기록

국비 지원 개발자 과정_Day90

by 루팽 2023. 4. 6.

<리덕스 로그인 로그아웃과 토스트 - 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

댓글