인프런 ==> https://www.inflearn.com/course/따라하며-배우는-노드-리액트-영화사이트-만들기#reviews
유튜브 강의 목록 : https://www.youtube.com/playlist?list=PL9a7QRYt5fqkowXUgTj_tbkFClsPhO5XV
강의 파일 : https://braverokmc79.github.io/react_movie_clone/movie_clone.html
Boiler Plate 소스 :
https://github.com/jaewonhimnae/boilerplate-mern-stack
https://github.com/braverokmc79/react_boiler_plate
완성본 소스 (John Ahn) : https://github.com/jaewonhimnae/react-movie-app-ko
소스 : https://github.com/braverokmc79/react_movie_clone
강의 목록
1. 소개 영상
2. Boiler-Plate & MongoDB 연결
3. The MovieDB API 설명
4. Landing Page 만들기 (1)
5. Grid Card Component
6. Load More Button 만들기
7. Movie Detail 페이지 만들기
8. 영화 출연진들 가져오기
9. Favorite 버튼 만들기 (1)
10. Favorite 버튼 만들기 (2)
11. Favorite Button 만들기 (3)
12. Favorite list에 추가 삭제
13. Favorite 페이지 만들기 (1)
14. Favorite 페이지 (2)
15. 강의 마무리
vscode 확장 패키지 추가
1. - Auto Import - ES6, TS, JSX, TSX
2. - Reactjs code snippets
3. - ESLint
4. - Prettier - Code formatter
Visual Studio Code 폴더/파일 아이콘 변경하기
리액트 프로젝트 생성
npx create-react-app 경로
예) npx create-react-app E:\react-app2
넥스트 프로젝트 생성
$ npx create-next-app nextjs-tutorial
1. 소개 영상
2. Boiler-Plate & MongoDB 연결
React 유튜브 클론 시리즈 ( John Ahn) ) 1 - Boiler Plate 설정
https://macaronics.net/m04/react/view/1911
Boiler Plate 소스 :
1) https://github.com/jaewonhimnae/boilerplate-mern-stack
2) https://github.com/braverokmc79/react_boiler_plate
1 또는 2 번 소스 다운로드 받은 후 진행
2번 소스인 경우
# 실행 방법
1. server 에 디렉토리에서 yarn install
2. client 에 디렉토리에서 yarn install
3. server 에 디렉토리에 npm run dev
config 디렉토리에서 dev.js 파일 생성
몽고 DB
module.exports = { mongoURI: "mongodb+srv://macaronics:<password>@mongo-macaronics.y37mjuf.mongodb.net/movie_app" }
3. The MovieDB API 설명
themoviedb 사이트 등록 하기
=> https://www.themoviedb.org/
리액트 : client/src/components/Config.js
export const USER_SERVER = '/api/users'; export const API_URL = "https://api.themoviedb.org/3/"; export const IMAGE_BASE_URL = "https://image.tmdb.org/t/p/";
문서
https://www.themoviedb.org/documentation/api/discover
4. Landing Page 만들기 (1)
Config.js
export const USER_SERVER = '/api/users'; export const API_URL = "https://api.themoviedb.org/3/"; export const IMAGE_BASE_URL = "https://image.tmdb.org/t/p/";
MovieApiKey.js
// API 키(v3 auth) export const MOVIE_API_KEY = "키값"; // API 요청 예 // https://api.themoviedb.org/3/movie/550?api_key=08d90cc4e7968b1f8e51588a0d42cf06 // API 읽기 액세스 토큰(v4 auth) export const MOVIE_API_TOKEN = "토큰키";
LandingPage.js
import React, { useEffect } from 'react' import { API_URL, IMAGE_BASE_URL } from '../../Config'; import { MOVIE_API_KEY } from '../../MovieApiKey'; import { useState } from 'react'; import MainImage from './Sections/MainImage'; function LandingPage() { const [Movies, setMovies] = useState([]); const [MainMovieImage, setMainMovieImage] = useState(null); useEffect(() => { const endPoint = `${API_URL}movie/popular?api_key=${MOVIE_API_KEY}&language=en-US&page=1`; fetch(endPoint) .then(res => res.json()) .then(data => { setMovies(data.results); setMainMovieImage(data.results[0]); }) .catch(err => { console.log("에러 :", err); }); }, []); return ( <div style={{ width: '100%', margin: '0' }}> {/* Main Image */} {MainMovieImage && <MainImage title={MainMovieImage.title} text={MainMovieImage.overview} image={`${IMAGE_BASE_URL}original${MainMovieImage.backdrop_path}`} /> } <div style={{ width: '85%', margin: '1rem auth', position: "relative", left: "7.5%" }}> <h2>Movies by latest</h2> <hr /> { Movies.map((movie, index) => ( <div key={index}> <img src={`${IMAGE_BASE_URL}w500${movie.backdrop_path}`} /> </div> )) } {/* Movie Grid Cards */} </div> <div style={{ display: 'flex', justifyContent: 'center' }}> <button>Load More</button> </div> </div> ) } export default LandingPage
MainImage.js
import React from 'react' function MainImage(props) { return ( <div style={{ background: `linear-gradient(to bottom, rgba(0,0,0,0) 39%, rgba(0,0,0,0) 41%, rgba(0,0,0,0.65) 100%), url('${props.image}') , #1c1c1c`, backgroundSize: "100% 100%", height: "500px", backgroundPosition: 'center, center', width: '100%', position: 'relative' }}> <div style={{ position: "absolute", maxWidth: '500px', bottom: '2rem', marginLeft: '2rem' }}> <h2 style={{ color: 'white' }}>{props.title}</h2> <p style={{ color: 'white', fontSize: '1rem' }}>{props.text}</p> </div> </div> ) } export default MainImage
5. Grid Card Component
LandingPage.js
import React, { useEffect } from 'react' import { API_URL, IMAGE_BASE_URL } from '../../Config'; import { MOVIE_API_KEY } from '../../MovieApiKey'; import { useState } from 'react'; import MainImage from './Sections/MainImage'; import GridCards from './../commons/GridCards'; import { Row } from 'antd'; function LandingPage() { const [Movies, setMovies] = useState([]); const [MainMovieImage, setMainMovieImage] = useState(null); useEffect(() => { const endPoint = `${API_URL}movie/popular?api_key=${MOVIE_API_KEY}&language=en-US&page=1`; fetch(endPoint) .then(res => res.json()) .then(data => { setMovies(data.results); setMainMovieImage(data.results[0]); console.log(data.results); }) .catch(err => { console.log("에러 :", err); }); }, []); return ( <div style={{ width: '100%', margin: '0' }}> {/* Main Image */} {MainMovieImage && <MainImage title={MainMovieImage.title} text={MainMovieImage.overview} image={`${IMAGE_BASE_URL}original${MainMovieImage.backdrop_path}`} /> } <div style={{ width: '85%', margin: '1rem auth', position: "relative", left: "7.5%" }}> <h2>Movies by latest</h2> <hr /> {/* Movie Grid Cards */} <Row gutter={[16, 48]} > {Movies && Movies.map((movie, index) => ( <React.Fragment key={index}> <GridCards image={movie.poster_path ? `${IMAGE_BASE_URL}w500${movie.poster_path}` : null} movieId={movie.id} movieName={movie.original_title} /> </React.Fragment> )) } </Row> </div> <div style={{ display: 'flex', justifyContent: 'center' }}> <button>Load More</button> </div> </div> ) } export default LandingPage
MainImage.js
import React from 'react' function MainImage(props) { return ( <div style={{ background: `linear-gradient(to bottom, rgba(0,0,0,0) 39%, rgba(0,0,0,0) 41%, rgba(0,0,0,0.65) 100%), url('${props.image}') , #1c1c1c`, backgroundSize: "100% 100%", height: "500px", backgroundPosition: 'center, center', width: '100%', position: 'relative' }}> <div style={{ position: "absolute", maxWidth: '500px', bottom: '2rem', marginLeft: '2rem' }}> <h2 style={{ color: 'white' }}>{props.title}</h2> <p style={{ color: 'white', fontSize: '1rem' }}>{props.text}</p> </div> </div> ) } export default MainImage
GridCards.js
import React from 'react'; import { Col } from 'antd'; import { Link } from 'react-router-dom'; import style from './GridCards.css'; function GridCards(props) { return ( <Col lg={6} md={8} xs={24}> <div style={{ position: 'relative' }} > <Link to={`/movie/${props.movieId}`}> <img src={props.image} alt={props.movieName} className="img" style={{ width: "80%", height: "auto" }} /> </Link> </div> </Col> ) } export default GridCards
GridCards.css
.img{ box-shadow: 5px 5px 5px rgb(0 0 0 / 20%); } .img:hover{ box-shadow: 5px 5px 5px rgb(0 0 0 / 80%); }
6. Load More Button 만들기
LandingPage.js
import React, { useEffect } from 'react' import { API_URL, IMAGE_BASE_URL } from '../../Config'; import { MOVIE_API_KEY } from '../../MovieApiKey'; import { useState } from 'react'; import MainImage from './Sections/MainImage'; import GridCards from './../commons/GridCards'; import { Row, Button } from 'antd'; function LandingPage() { const [Movies, setMovies] = useState([]); const [MainMovieImage, setMainMovieImage] = useState(null); const [CurrentPage, setCurrentPage] = useState(0); useEffect(() => { const endPoint = `${API_URL}movie/popular?api_key=${MOVIE_API_KEY}&language=en-US&page=1`; fetchMovies(endPoint); }, []); const fetchMovies = (endPoint) => { fetch(endPoint) .then(res => res.json()) .then(data => { setMovies([...Movies, ...data.results]); setMainMovieImage(data.results[0]); setCurrentPage(data.page); }) .catch(err => { console.log("에러 :", err); }); } const loadMoreItems = () => { const endPoint = `${API_URL}movie/popular?api_key=${MOVIE_API_KEY}&language=en-US&page=${CurrentPage + 1}`; fetchMovies(endPoint); } return ( <> <div style={{ width: '100%', margin: '0' }}> {MainMovieImage && <MainImage title={MainMovieImage.title} text={MainMovieImage.overview} image={`${IMAGE_BASE_URL}original${MainMovieImage.backdrop_path}`} /> } </div> <div style={{ width: '100%', margin: '0', padding: '10px 5%' }}> <div style={{ width: '100%', margin: '1rem auth', }}> <h2>Movies by latest</h2> <hr /> {/* Movie Grid Cards */} <Row gutter={[16, 48]} > {Movies && Movies.map((movie, index) => ( <React.Fragment key={index}> {movie.poster_path && <GridCards path={movie.poster_path} image={movie.poster_path ? `${IMAGE_BASE_URL}w500${movie.poster_path}` : null} movieId={movie.id} movieName={movie.original_title} /> } </React.Fragment> )) } </Row> <div style={{ textAlign: 'center', margin: "100px 0" }}> <Button onClick={loadMoreItems}>더보기</Button> </div> </div> </div> </> ) } export default LandingPage
7. Movie Detail 페이지 만들기
https://api.themoviedb.org/3/
${API_URL}movie/${movieId}?api_key=${MOVIE_API_KEY}
- adult: false
- backdrop_path: "/wDyM1lIIgK4RIDAgr8iuZe9N1cf.jpg"
- belongs_to_collection: null
- budget: 0
- genres: (4) [{…}, {…}, {…}, {…}]
- homepage: "https://www.netflix.com/title/81186049"
- id: 755566
- imdb_id: "tt13314558"
- original_language: "en"
- original_title: "Day Shift"
- overview: "An LA vampire hunter has a week to come up with the cash to pay for his kid's tuition and braces. Trying to make a living these days just might kill him."
- popularity: 1320.051
- poster_path: "/bI7lGR5HuYlENlp11brKUAaPHuO.jpg"
- production_companies: (2) [{…}, {…}]
- production_countries: [{…}]
- release_date: "2022-08-10"
- revenue: 0
- runtime: 113
- spoken_languages: [{…}]
- status: "Released"
- tagline: "Some jobs really go for the throat."
- title: "Day Shift"
- video: false
- vote_average: 6.91
- vote_count: 912
MovieInfo.js
import React from 'react' import { Descriptions, Badge } from 'antd'; function MovieInfo(props) { let { movie } = props; return ( <Descriptions title="Movie Info" bordered> <Descriptions.Item label="Title">{movie.original_title}</Descriptions.Item> <Descriptions.Item label="release_date">{movie.release_date}</Descriptions.Item> <Descriptions.Item label="revenue">{movie.revenue}</Descriptions.Item> <Descriptions.Item label="runtime">{movie.runtime}</Descriptions.Item> <Descriptions.Item label="vote_average" span={2}> {movie.vote_average} </Descriptions.Item> <Descriptions.Item label="vote_count">{movie.vote_count}</Descriptions.Item> <Descriptions.Item label="status">{movie.status}</Descriptions.Item> <Descriptions.Item label="popularity">{movie.popularity}</Descriptions.Item> </Descriptions> ) } export default MovieInfo
MovieDetail.js
import { Button } from 'antd'; import React, { useEffect, useState } from 'react' import { useParams } from 'react-router-dom' import { API_URL, IMAGE_BASE_URL } from '../../Config'; import { MOVIE_API_KEY } from '../../MovieApiKey'; import MainImage from '../LandingPage/Sections/MainImage'; import MovieInfo from './Sections/MovieInfo'; function MovieDetail(props) { const { movieId } = useParams(); const [Movie, setMovie] = useState([]) useEffect(() => { let endpointCrew = `${API_URL}movie/${movieId}/credits?api_key=${MOVIE_API_KEY}`; let endpointInfo = `${API_URL}movie/${movieId}?api_key=${MOVIE_API_KEY}`; fetch(endpointInfo) .then(res => res.json()) .then(data => { console.log("data: ", data); setMovie(data); }).catch(error => { console.error("에러 : ", error); }) }, []) return ( <div> {/* Header */} <div style={{ width: '100%', margin: '0' }}> {Movie && <MainImage title={Movie.title} text={Movie.overview} image={`${IMAGE_BASE_URL}original${Movie.backdrop_path}`} /> } </div> {/* Body */} <div style={{ width: '85%', margin: '1rem auto' }}> {/* Movie Info */} <MovieInfo movie={Movie} /> <br /> {/* Actors Grid */} <div style={{ display: 'flex', justifyContent: 'center', margin: '2rem' }}> <Button>Toggle Actor View </Button> </div> </div> </div> ) } export default MovieDetail
8. 영화 출연진들 가져오기
src\components\views\LandingPage\LandingPage.js
landingPage={true} 추가
~~ <GridCards path={movie.poster_path} landingPage={true} image={movie.poster_path ? `${IMAGE_BASE_URL}w500${movie.poster_path}` : null} movieId={movie.id} movieName={movie.original_title} /> ~~
src\components\views\MovieDetail\MovieDetail.js
import { Button, Row } from 'antd'; import React, { useEffect, useState } from 'react' import { useParams } from 'react-router-dom' import { API_URL, IMAGE_BASE_URL } from '../../Config'; import { MOVIE_API_KEY } from '../../MovieApiKey'; import GridCards from '../commons/GridCards'; import MainImage from '../LandingPage/Sections/MainImage'; import MovieInfo from './Sections/MovieInfo'; function MovieDetail(props) { const { movieId } = useParams(); const [Movie, setMovie] = useState([]); const [Casts, setCasts] = useState([]); const [ActorToggle, setActorToggle] = useState(false); useEffect(() => { let endpointCrew = `${API_URL}movie/${movieId}/credits?api_key=${MOVIE_API_KEY}`; let endpointInfo = `${API_URL}movie/${movieId}?api_key=${MOVIE_API_KEY}`; fetch(endpointInfo) .then(res => res.json()) .then(data => { console.log("endpointInfo: ", data); setMovie(data); }).catch(error => { console.error("에러 : ", error); }) fetch(endpointCrew) .then(res => res.json()) .then(data => { console.log("endpointCrew - cast: ", data.cast); setCasts(data.cast); }).catch(error => { console.error("에러 : ", error); }) }, []) return ( <div> {/* Header */} <div style={{ width: '100%', margin: '0' }}> {Movie && Movie.backdrop_path !== undefined && <MainImage title={Movie.title} text={Movie.overview} image={`${IMAGE_BASE_URL}original/${Movie.backdrop_path}`} /> } </div> {/* Body */} <div style={{ width: '85%', margin: '1rem auto' }}> {/* Movie Info */} <MovieInfo movie={Movie} /> <br /> {/* Actors Grid */} <div style={{ display: 'flex', justifyContent: 'center', margin: '2rem' }}> <Button onClick={() => (setActorToggle(!ActorToggle))}> Toggle Actor View </Button> </div> {/* console.log("endpointCrew - cast: ", data.cast); adult: false cast_id: 1 character: "" credit_id: "5f8f1cec383df20034aab300" gender: 2 id: 134 known_for_department: "Acting" name: "Jamie Foxx" order: 0 original_name: "Jamie Foxx" popularity: 33.058 profile_path: "/hPwCMEq6jLAidsXAX5BfoYgIfg2.jpg" */} {ActorToggle && <Row gutter={[16, 48]} > {Casts && Casts.map((cast, index) => ( <React.Fragment key={index}> {cast.profile_path && <GridCards path={cast.profile_path} image={cast.profile_path ? `${IMAGE_BASE_URL}w500${cast.profile_path}` : null} characterName={cast.name} /> } </React.Fragment> )) } </Row> } </div> </div> ) } export default MovieDetail
src\components\views\commons\GridCards.js
import React from 'react'; import { Col } from 'antd'; import { Link } from 'react-router-dom'; import style from './GridCards.css'; function GridCards(props) { if (props.landingPage) { return ( <Col lg={6} md={8} xs={24}> <div style={{ position: 'relative', textAlign: 'center' }} > <Link to={`/movie/${props.movieId}`}> <img src={props.image} alt={props.movieName} className="img" style={{ width: "80%", height: "auto", maxHeight: '400px' }} /> </Link> </div> </Col> ) } else { return ( <Col lg={6} md={8} xs={24}> <div style={{ position: 'relative', textAlign: 'center' }} > <img src={props.image} alt={props.characterName} className="img" style={{ width: "80%", height: "auto", maxHeight: '400px' }} /> </div> </Col> ) } } export default GridCards
댓글 ( 4)
댓글 남기기