Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[서버 사이드 렌더링 - 2단계] 빙봉(김윤경) 미션 제출합니다. #55

Merged
merged 18 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8c9afb0
fix: 뼈대 코드 수정 및 샘플 코드 제공
woowahan-cron Oct 8, 2024
4e15e61
feat: MovieList 컴포넌트 구현
Yoonkyoungme Oct 9, 2024
fac0400
feat: Header 컴포넌트 구현
Yoonkyoungme Oct 9, 2024
1e7baf7
feat: Footer 컴포넌트 구현
Yoonkyoungme Oct 9, 2024
ec57e9e
feat: Header, MovieList, Footer 컴포넌트를 App 컴포넌트에 추가
Yoonkyoungme Oct 9, 2024
6ef8fa5
feat: TMDB API 호출을 위한 상수 및 fetchMovieItems 함수 추가
Yoonkyoungme Oct 9, 2024
4c90147
fix: 서버와 클라이언트 초기 데이터를 동기화하여 Hydration 오류 해결
Yoonkyoungme Oct 9, 2024
66c7c3d
chore: react-router-dom 설치
Yoonkyoungme Oct 19, 2024
46e2a34
feat: 영화 상세 정보를 가져오는 fetchMovieDetail 함수 추가
Yoonkyoungme Oct 19, 2024
bb093c2
클라이언트에서 환경 변수 사용이 가능하게 webpack 설정 추가
Yoonkyoungme Oct 19, 2024
60b8dfc
feat: 영화 상세 정보를 보여주는 모달 컴포넌트 구현
Yoonkyoungme Oct 19, 2024
4298145
feat: MoviePage, MovieDetailPage 구현
Yoonkyoungme Oct 19, 2024
56269d7
feat: 클라이언트 라우팅 설정 및 적용
Yoonkyoungme Oct 19, 2024
f003fc4
feat: 영화 상세 페이지 서버사이드 렌더링 추가
Yoonkyoungme Oct 20, 2024
3e85552
feat: 클라이언트에서 영화 상세 정보 페칭 로직 구현
Yoonkyoungme Oct 20, 2024
910a2f4
Merge branch 'yoonkyoungme' into step2
Yoonkyoungme Oct 21, 2024
48477ed
fix: Header 배경 이미지 url 수정
Yoonkyoungme Oct 21, 2024
7562ba0
remove: 중복 선언한 apis 디렉터리 삭제
Yoonkyoungme Oct 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"dependencies": {
"express": "^4.21.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"react-router-dom": "^6.27.0"
},
"devDependencies": {
"@babel/preset-env": "^7.25.4",
Expand Down
12 changes: 5 additions & 7 deletions src/client/App.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import React from "react";
import { Outlet } from "react-router-dom";

import MovieList from "./components/MovieList";
import Header from "./components/Header";
import Footer from "./components/Footer";

function App({ movies }) {
function App() {
return (
<div id="wrap">
<Header bestMovie={movies[0]} />
<MovieList movies={movies} />
<>
<Outlet />
<Footer />
</div>
</>
);
}

Expand Down
15 changes: 11 additions & 4 deletions src/client/components/Header.jsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import React from "react";
import { TMDB_THUMBNAIL_URL } from "../constants";
import { useNavigate } from "react-router-dom";

import logoImage from "@images/logo.png";
import starEmptyImage from "@images/star_empty.png";

export default function Header({ bestMovie }) {
const navigate = useNavigate();

if (!bestMovie || bestMovie.length === 0) {
return null;
}

const { poster_path, vote_average, title } = bestMovie;
const { id, backdrop_path, vote_average, title } = bestMovie;

return (
<header>
<div
className="background-container"
style={{
backgroundImage: `url('${TMDB_THUMBNAIL_URL}/${poster_path}')`,
backgroundImage: `url(https://image.tmdb.org/t/p/w1920_and_h800_multi_faces${backdrop_path})`,
}}
>
<div className="overlay" aria-hidden="true"></div>
Expand All @@ -30,7 +32,12 @@ export default function Header({ bestMovie }) {
<span className="rate-value">{vote_average.toFixed(1)}</span>
</div>
<div className="title">{title}</div>
<button className="primary detail">자세히 보기</button>
<button
className="primary detail"
onClick={() => navigate(`/detail/${id}`)}
>
자세히 보기
</button>
</div>
</div>
</div>
Expand Down
34 changes: 16 additions & 18 deletions src/client/components/MovieList.jsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
import React from "react";
import { useNavigate } from "react-router-dom";

import { TMDB_THUMBNAIL_URL } from "../constants";

import starEmptyImage from "@images/star_empty.png";

export default function MovieList({ movies }) {
return (
<div className="container">
<main>
<section>
<h2>지금 인기 있는 영화</h2>
{movies && (
<ul className="thumbnail-list">
{movies.map((movie) => (
<li key={movie.id}>
<MovieItem movie={movie} />
</li>
))}
</ul>
)}
</section>
</main>
</div>
<>
{movies && (
<ul className="thumbnail-list">
{movies.map((movie) => (
<li key={movie.id}>
<MovieItem movie={movie} />
</li>
))}
</ul>
)}
</>
);
}

function MovieItem({ movie }) {
const { title, poster_path, vote_average } = movie;
const navigate = useNavigate();

const { id, title, poster_path, vote_average } = movie;

return (
<div className="item">
<div className="item" onClick={() => navigate(`/detail/${id}`)}>
<img
className="thumbnail"
src={`${TMDB_THUMBNAIL_URL}/${poster_path}`}
Expand Down
67 changes: 67 additions & 0 deletions src/client/components/MovieModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";

import { fetchMovieDetail } from "../../common/apis/movies";

import { TMDB_THUMBNAIL_URL } from "../constants";
import starEmptyImage from "@images/star_empty.png";

const MovieModal = ({ movie: initialMovie }) => {
const navigate = useNavigate();
const { id: movieId } = useParams();

const [movie, setMovie] = useState(initialMovie || null);

useEffect(() => {
const fetchMovie = async () => {
const data = await fetchMovieDetail(movieId);

setMovie(data);
};

if (!movie) {
fetchMovie();
}
}, [movie, movieId]);

if (!movie) {
return <div>Loading...</div>;
}

const { title, poster_path, genres, vote_average, overview } = movie;

return (
movie && (
<div className="modal-background active" id="modalBackground">
<div className="modal">
<button
className="close-modal"
id="closeModal"
onClick={() => navigate("/")}
>
<img src="/static/images/modal_button_close.png" alt="Close" />
</button>
<div className="modal-container">
<div className="modal-image">
<img src={`${TMDB_THUMBNAIL_URL}${poster_path}`} alt={title} />
</div>
<div className="modal-description">
<h2>{title}</h2>
<p className="category">
{genres.map((genre) => genre.name).join(", ")}
</p>
<p className="rate">
<img src={starEmptyImage} className="star" alt="Rating" />
<span>{vote_average.toFixed(1)}</span>
</p>
<hr />
<p className="detail">{overview}</p>
</div>
</div>
</div>
</div>
)
);
};

export default MovieModal;
11 changes: 8 additions & 3 deletions src/client/main.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import React from "react";

import { hydrateRoot } from "react-dom/client";
import App from "./App";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import routes from "./routes";

const { movies } = window.__INITIAL_DATA__;
const router = createBrowserRouter(routes);

hydrateRoot(document.getElementById("root"), <App movies={movies} />);
hydrateRoot(
document.getElementById("root"),
<RouterProvider router={router} />
);
13 changes: 13 additions & 0 deletions src/client/pages/MovieDetailPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from "react";

import MovieModal from "../components/MovieModal";
import MoviePage from "./MoviePage";

export default function MovieDetailPage({ movies, movie }) {
return (
<>
<MoviePage movies={movies} />
<MovieModal movie={movie} />
</>
);
}
20 changes: 20 additions & 0 deletions src/client/pages/MoviePage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from "react";

import Header from "../components/Header";
import MovieList from "../components/MovieList";

export default function MoviePage({ movies }) {
return (
<>
<Header bestMovie={movies[0]} />
<div className="container">
<main>
<section>
<h2>지금 인기 있는 영화</h2>
<MovieList movies={movies} />
</section>
</main>
</div>
</>
);
}
26 changes: 26 additions & 0 deletions src/client/routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from "react";

import App from "./App";
import MoviePage from "./pages/MoviePage";
import MovieDetailPage from "./pages/MovieDetailPage";

const { movies, movie } = window.__INITIAL_DATA__ || {};

const routes = [
{
path: "/",
element: <App />,
children: [
{
index: true,
element: <MoviePage movies={movies} />,
},
{
path: "detail/:id",
element: <MovieDetailPage movies={movies} movie={movie} />,
},
],
},
];

export default routes;
2 changes: 2 additions & 0 deletions src/server/apis/constants.js → src/common/apis/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export const TMDB_MOVIE_LISTS = {
nowPlaying: BASE_URL + "/now_playing?language=ko-KR&page=1",
};

export const TMDB_MOVIE_DETAIL_URL = "https://api.themoviedb.org/3/movie/";

export const FETCH_OPTIONS = {
method: "GET",
headers: {
Expand Down
21 changes: 21 additions & 0 deletions src/common/apis/movies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {
TMDB_MOVIE_LISTS,
FETCH_OPTIONS,
TMDB_MOVIE_DETAIL_URL,
} from "./constants.js";

export const fetchMovieItems = async (category = "nowPlaying") => {
const url = TMDB_MOVIE_LISTS[category];
const response = await fetch(url, FETCH_OPTIONS);
const data = await response.json();

return data;
};

export const fetchMovieDetail = async (id) => {
const url = `${TMDB_MOVIE_DETAIL_URL}${id}?language=ko-KR`;
const response = await fetch(url, FETCH_OPTIONS);
const data = await response.json();

return data;
};
9 changes: 0 additions & 9 deletions src/server/apis/movies.js

This file was deleted.

2 changes: 1 addition & 1 deletion src/server/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ app.use("/static", (req, res) => {
});

// 메인 페이지 라우트 (React 앱 렌더링)
app.get("/", movieRouter);
app.use("/", movieRouter);

// 그 외 모든 경로에 대한 404 처리
app.use((req, res) => {
Expand Down
Loading