#2 boiler-plate & mongodb 연결
- client: react
- server: node.js
#3 The moviedb API 설명
api 사용 시에 동일하게 계속 이용되는 부분은 아예 constant로 값 저장해놓고 사용하자
#4 landing page 만들기(1)
1. 전체적인 template 간단하게 만들기
2. Movie API에서 가져온 모든 데이터를 STATE에 넣기
3. MainImage Component 만들기
4. Grid Card Component 만들기
5. Load More Function 만들기
[1] [HPM] Proxy created: / -> http://localhost:5000 [1] i 「wds」: Project is running at http://172.30.1.28/ [1] i 「wds」: webpack output is served from [1] i 「wds」: Content not from webpack is served from C:\2022-prj\boilerplate-mern-stack-master\client\public [1] i 「wds」: 404s will fallback to / [1] Starting the development server... [1] [1] Browserslist: caniuse-lite is outdated. Please run: [1] npx browserslist@latest --update-db [1] Compiled with warnings. [1] [1] ./src/components/views/LandingPage/LandingPage.js [1] Line 2:10: 'FaCode' is defined but never used no-unused-vars [1] [1] ./src/components/views/LoginPage/LoginPage.js [1] Line 74:11: 'dirty' is assigned a value but never used no-unused-vars [1] Line 79:11: 'handleReset' is assigned a value but never used no-unused-vars [1] [1] ./src/components/views/RegisterPage/RegisterPage.js [1] Line 92:11: 'dirty' is assigned a value but never used no-unused-vars [1] Line 97:11: 'handleReset' is assigned a value but never used no-unused-vars [1] [1] Search for the keywords to learn more about each warning. [1] To ignore, add // eslint-disable-next-line to the line before. [1]
npx browserslist@latest --update-db
(react가 설치된, 실행되는 폴더로 터미널 이동해서 위 명령어 입력, 내 경우엔 client폴더로 이동)
{MainMovieImage && (
<MainImage
image={`${IMAGE_BASE_URL}w1280${MainMovieImage.backdrop_path}`}
title={MainMovieImage.original_title}
text={MainMovieImage.overview}
/>
)}
MainMovieImage 데이터 가져온 다음 렌더링하고자 할 때 위와 같은 식으로 코드 작성
#5 Grid Card Component
const [Movies, setMovies] = useState([]);
const [MainMovieImage, setMainMovieImage] = useState(null);
useEffect(() => {
const endpoint = `${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=1`;
fetch(endpoint)
.then((response) => response.json())
.then((response) => {
console.log(response.results);
setMovies(response.results);
setMainMovieImage(response.results[0]);
});
}, []);
grid card 영화들이 안떠서 setMovies([response.results])를
setMovies(response.result)로 수정하니 제대로 작동
response.results 자체가 이미 배열이여서 그런 듯..? 그런데 강의는 어떻게 된건지...
#6 Load More Button
const [Movies, setMovies] = useState([]);
const [MainMovieImage, setMainMovieImage] = useState(null);
const [CurrentPage, setCurrentPage] = useState(0);
useEffect(() => {
const endpoint = `${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=1`;
fetchMovies(endpoint);
}, []);
const fetchMovies = (endpoint) => {
fetch(endpoint)
.then((response) => response.json())
.then((response) => {
console.log(response);
setMovies([...Movies, ...response.results]); //여기 배열 주목
setMainMovieImage(response.results[0]);
setCurrentPage(response.page);
});
};
const loadMoreItems = () => {
const endpoint = `${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=${
CurrentPage + 1
}`;
fetchMovies(endpoint);
};
useState 훅을 이용해서 CurrentPage라는 state과 setCurrentPage라는 state변경함수를 생성한다. 본래 themoviedb의 특정 페이지에 존재하는 영화 리스트들을 받아와 화면에 보여주었는데, 이 페이지 수를 하나씩 늘려가면서 영화 리스트들을 추가로 렌더링해 보여주기 위해서 state을 이용한다. <button>의 onClick속성에 loadMoreItems 함수를 지정하여 버튼을 클릭하면 영화 리스트들이 추가로 보여진다.
또 위 코드들 중 fetchMovies 함수에서
setMovies[...Movies, ...response.results])
이 코드를 주의해서 봐야한다. 그냥 setMovies(response.result)라고 하면 새로 받아온 영화 리스트들만 보여진다. 하지만 우리는 처음 받아온 영화 리스트에 새로 받아온 영화 리스트를 추가하여 보여줄 것이므로 위와 같이 작성해야 한다.
#7 Movie Detail 페이지 만들기
//MovieDetail.js
import React, { useEffect, useState } from "react";
import { API_URL, API_KEY, IMAGE_BASE_URL } from "../../Config";
import MainImage from "../LandingPage/Sections/MainImage";
import MovieInfo from "./Sections/MovieInfo";
function MovieDetail(props) {
let movieId = props.match.params.movieId;
const [Movie, setMovie] = useState([]);
useEffect(() => {
let endpointCrew = `${API_URL}movie/${movieId}/credits?api_key=${API_KEY}`;
let endpointInfo = `${API_URL}movie/${movieId}?api_key=${API_KEY}`;
fetch(endpointInfo)
.then((response) => response.json())
.then((response) => {
console.log(response);
setMovie(response);
});
});
return (
<div>
{/* Header */}
<MainImage
image={`${IMAGE_BASE_URL}w1280${Movie.backdrop_path}`}
title={Movie.original_title}
text={Movie.overview}
/>
{/* Body */}
<div style={{ width: "85%", margin: "1rem auto" }}>
{/* Movie Info */}
<MovieInfo movie={Movie} />
<br />
{/* Actors Grid */}
</div>
<div
style={{ display: "flex", justifyContent: "center", margin: "2rem" }}
>
<button>Toggle Actor View</button>
</div>
</div>
);
}
export default MovieDetail;
//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;
랜딩페이지에 보여지는 영화 리스트 중 특정 영화(이미지)를 클릭하면 해당 영화의 상세 정보가 보여지는 페이지로 이동한다.
이 과정이 가능하게 하는 코드를 살펴보려면 GridCard 컴포넌트를 봐보자.
//GridCard.js
import React from "react";
import { Col } from "antd";
function GridCards(props) {
return (
<Col lg={6} md={8} xs={24}>
<div style={{ position: "relative" }}>
<a href={`/movie/${props.movieId}`}>
<img
style={{ width: "100%", height: "320px" }}
src={props.image}
alt={props.movieName}
/>
</a>
</div>
</Col>
);
}
export default GridCards;
이 코드를 살펴보면 영화 이미지를 <a>태그로 감싸서 href속성을 달아놓았다. 그래서 /movie/:movieId 페이지로 이동할 수 있는 것이다.
그리고 이 페이지을 실제로 이용하기 위해서는 app.js 파일에 해당 url을 등록해주어야 하는데,
//app.js
import React, { Suspense } from "react";
import { Route, Switch } from "react-router-dom";
import Auth from "../hoc/auth";
// pages for this product
import LandingPage from "./views/LandingPage/LandingPage.js";
import LoginPage from "./views/LoginPage/LoginPage.js";
import RegisterPage from "./views/RegisterPage/RegisterPage.js";
import NavBar from "./views/NavBar/NavBar";
import Footer from "./views/Footer/Footer";
import MovieDetail from "./views/MovieDetail/MovieDetail";
//null Anyone Can go inside
//true only logged in user can go inside
//false logged in user can't go inside
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<NavBar />
<div style={{ paddingTop: "69px", minHeight: "calc(100vh - 80px)" }}>
<Switch>
<Route exact path="/" component={Auth(LandingPage, null)} />
<Route exact path="/login" component={Auth(LoginPage, false)} />
<Route exact path="/register" component={Auth(RegisterPage, false)} />
<Route
exact
path="/movie/:movieId"
component={Auth(MovieDetail, null)}
/>
</Switch>
</div>
<Footer />
</Suspense>
);
}
export default App;
위 코드 중
<Route
exact
path="/movie/:movieId"
component={Auth(MovieDetail, null)}
/>
이 부분을 주의해서 보면 된다.
#8 영화 출연진들 가져오기
//MovieDetail.js
import React, { useEffect, useState } from "react";
import { API_URL, API_KEY, IMAGE_BASE_URL } from "../../Config";
import MainImage from "../LandingPage/Sections/MainImage";
import MovieInfo from "./Sections/MovieInfo";
import GridCards from "../commons/GridCards";
import { Row } from "antd";
function MovieDetail(props) {
let movieId = props.match.params.movieId;
const [Movie, setMovie] = useState([]);
const [Casts, setCasts] = useState([]);
const [ActorToggle, setActorToggle] = useState(false);
useEffect(() => {
let endpointCrew = `${API_URL}movie/${movieId}/credits?api_key=${API_KEY}`;
let endpointInfo = `${API_URL}movie/${movieId}?api_key=${API_KEY}`;
fetch(endpointInfo)
.then((response) => response.json())
.then((response) => {
console.log(response);
setMovie(response);
});
fetch(endpointCrew)
.then((response) => response.json())
.then((response) => {
setCasts(response.cast);
});
}, {});
const toggleActorView = () => {
setActorToggle(!ActorToggle);
};
return (
<div>
{/* Header */}
{Movie && (
<MainImage
image={`${IMAGE_BASE_URL}w1280${Movie.backdrop_path}`}
title={Movie.original_title}
text={Movie.overview}
/>
)}
{/* 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={toggleActorView}>Toggle Actor View </button>
</div>
{ActorToggle && (
<Row gutter={[16, 16]}>
{Casts &&
Casts.map((cast, index) => (
<React.Fragment key={index}>
<GridCards
image={
cast.profile_path
? `${IMAGE_BASE_URL}w500${cast.profile_path}`
: null
}
characterName={cast.name}
/>
</React.Fragment>
))}
</Row>
)}
</div>
</div>
);
}
export default MovieDetail;
배우 출력 관련 주로 봐야하는 것은
- useState훅으로 [Cast, setCast]
- useState훅으로 [ActorToggle, setActorToggle]
를 생성하여 각 state으로 cast데이터 정보 받아오고, 버튼 state이 바뀌면서 cast정보 출력 여부를 결정한다는 것이다.
위 두개와 같은 warning이 뜨는데 뭐가 문젠지 알아봐야겠다....좀 이따가..
#9,10,11,12 Favorite 버튼 만들기, Favorite 리스트에서 영화 추가 및 삭제
1. Favorite Model 만들기
2. Favorite Button UI 만들기
3. 얼마나 많은 사람이 이 영화를 Favorite 리스트에 넣었는지 그 숫자 정보 얻기
4. 내가 이 영화를 이미 Favorite리스트에 넣었는지 아닌지 정보 얻기
5. 데이터를 화면에 보여주기
// server/models/favorite.js
const { Schema } = require("mongoose");
const mongoose = require("mongoose");
const favoriteSchema = mongoose.Schema(
{
userFrom: {
type: Schema.Types.ObjectId,
ref: "User",
},
movieId: {
type: String,
},
movieTitle: {
type: String,
},
moviePost: {
type: String,
},
movieRunTime: {
type: String,
},
},
{ timestamps: true }
);
const Favorite = mongoose.model("Favorite", favoriteSchema);
module.exports = { Favorite };
server 폴더에서 favorite 모델을 생성한다. favorite을 누른 사용자 정보도 저장하기 위해서 userFrom에서는 User모델을 참조하여 userId를 가져온다.
favoriteNumber 실행하니까 위 두 에러가 계속 뜸
1차시도: bcrypt 5.0.0으로 버전 바꿔봄 -> 안됨
2차시도: bcryptjs 라이브러리로 바꿔봄 -> 안됨, 다른 스키마 에러 메시지 뜸
3차시도: ..를 찾아보니 nodejs, vsc 재설치해서 해결했다는데..아놔.....전에는 잘 됐는데 왜 이러지..?
https://www.inflearn.com/questions/42246
저 역시 ECONNREFUSED 에러가 생깁니다. - 인프런 | 질문 & 답변
[HPM] Error occurred while trying to proxy request /api/users/auth from localhost:3000 to http://localhost:5000 (ECONNREFUSED) (https://nodejs.org/api/e...
www.inflearn.com
놀랍게도 단순히 오타로 인한 오류였다는 것...!두둥...(^^)
favorite 관련 api
const express = require("express");
const router = express.Router();
const { Favorite } = require("../models/Favorite");
router.post("/favoriteNumber", (req, res) => {
//mongodb에서 favorite숫자를 가져오기
Favorite.find({ movieId: req.body.movieId }).exec((err, info) => {
if (err) return res.status(400).send(err);
//그 다음에 프론트에 다시 숫자 정보 보내주기
res.status(200).json({ success: true, favoriteNumber: info.length });
});
});
router.post("/favorited", (req, res) => {
//내가 이 영화를 Favorite 리스트에 넣었는지 정보를 DB에서 가져오기
Favorite.find({
movieId: req.body.movieId,
userFrom: req.body.userFrom,
}).exec((err, info) => {
if (err) return res.status(400).send(err);
let result = false;
if (info.length !== 0) {
result = true;
}
res.status(200).json({ success: true, favorited: result });
});
});
router.post("/removeFromFavorite", (req, res) => {
Favorite.findOneAndDelete({
movieId: req.body.movieId,
userFrom: req.body.userFrom,
}).exec((err, doc) => {
if (err) return res.status(400).send(err);
return res.status(200).json({ success: true });
});
});
router.post("/addToFavorite", (req, res) => {
const favorite = new Favorite(req.body);
favorite.save((err, doc) => {
if (err) return res.status(400).send(err);
return res.status(200).json({ success: true });
});
});
module.exports = router;
client에서 데이터 받아오기
import React, { useEffect, useState } from "react";
import Axios from "axios";
import { Button } from "antd";
function Favorite(props) {
const movieId = props.movieId;
const userFrom = props.userFrom;
const movieTitle = props.movieInfo.title;
const moviePost = props.movieInfo.backdrop_path;
const movieRunTime = props.movieInfo.runtime;
const [FavoriteNumber, setFavoriteNumber] = useState(0);
const [Favorited, setFavorited] = useState(false);
let variables = {
userFrom: userFrom,
movieId: movieId,
movieTitle: movieTitle,
moviePost: moviePost,
movieRunTime: movieRunTime,
};
useEffect(() => {
Axios.post("/api/favorite/favoriteNumber", variables).then((response) => {
if (response.data.success) {
console.log(response.data);
setFavoriteNumber(response.data.favoriteNumber);
} else {
alert("숫자 정보를 가져오는데 실패했습니다.");
}
});
Axios.post("/api/favorite/favorited", variables).then((response) => {
if (response.data.success) {
console.log(response.data);
setFavorited(response.data.favorited);
} else {
alert("정보를 가져오는데 실패했습니다.");
}
});
});
const onClickFavorite = () => {
if (Favorited) {
Axios.post("/api/favorite/removeFromFavorite", variables).then(
(response) => {
if (response.data.success) {
setFavorited(!Favorited);
setFavoriteNumber(FavoriteNumber - 1);
} else {
alert("Favorite 리스트에서 지우는 것을 실패했습니다.");
}
}
);
} else {
Axios.post("/api/favorite/addToFavorite", variables).then((response) => {
if (response.data.success) {
setFavorited(!Favorited);
setFavoriteNumber(FavoriteNumber + 1);
} else {
alert("Favorite 리스트에 추가하는 것을 실패했습니다.");
}
});
}
};
return (
<div>
<Button onClick={onClickFavorite}>
{Favorited ? "Not Favorite" : "Add to favorite"} {FavoriteNumber}
</Button>
</div>
);
}
export default Favorite;
#13, 14 Favorite 페이지 만들기
favoirte movie 리스트 전달 api(server)
router.post("/getFavoriteMovie", (req, res) => {
Favorite.find({ userFrom: req.body.userFrom }).exec((err, favorites) => {
if (err) return res.status(400).send(err);
return res.status(200).json({ success: true, favorites });
});
});
favorite movie 페이지(client)
import React, { useEffect, useState } from "react";
import "./FavoritePage.css";
import Axios from "axios";
import { Popover } from "antd";
import { IMAGE_BASE_URL } from "../../Config";
function FavoritePage() {
const [Favorites, setFavorites] = useState([]);
useEffect(() => {
fetchFavoriteMovie();
}, []);
const fetchFavoriteMovie = () => {
Axios.post("/api/favorite/getFavoriteMovie", {
userFrom: localStorage.getItem("userId"),
}).then((response) => {
if (response.data.success) {
console.log(response.data);
setFavorites(response.data.favorites);
} else {
alert("영화 정보를 가져오는데 실패했습니다.");
}
});
};
const onClickDelete = (movieId, userFrom) => {
const variables = {
movieId,
userFrom,
};
Axios.post("api/favorite/removeFromFavorite", variables).then(
(response) => {
if (response.data.success) {
fetchFavoriteMovie();
} else {
alert("리스트에서 지우는데 실패했습니다.");
}
}
);
};
const renderCards = Favorites.map((favorite, index) => {
const content = (
<div>
{favorite.moviePost ? (
<img src={`${IMAGE_BASE_URL}w500${favorite.moviePost}`} />
) : (
"no image"
)}
</div>
);
return (
<tr key={index}>
<Popover content={content} title={`${favorite.movieTitle}`}>
<td>{favorite.movieTitle}</td>
</Popover>
<td>{favorite.movieRunTime} mins</td>
<td>
<button
onClick={() => onClickDelete(favorite.movieId, favorite.userFrom)}
>
Remove
</button>
</td>
</tr>
);
});
return (
<div style={{ width: "85%", margin: "3rem auto" }}>
<h2>FavoritePage</h2>
<hr />
<table>
<thead>
<tr>
<th>Movie Title</th>
<th>Movie RunTime</th>
<td>Remove from favorite</td>
</tr>
</thead>
<tbody>{renderCards}</tbody>
</table>
</div>
);
}
export default FavoritePage;
favorite movie remove버튼 눌렀을 때는 이전에 만들어놓은 removeFromFavorite api를 동일하게 사용한다.
그리고 remove버튼 클릭 후 화면에 변경된 리스트를 보이기 위해 영화리스트를 다시 불러와 fetch
<tbody>안에 영화를 renderCards 변수로 따로 만들어 놓은 거 확인하기
'React > 인프런' 카테고리의 다른 글
[(인프런)따라하며 배우는 노드 리액트 시리즈] 기본 강의 (0) | 2022.04.26 |
---|