From eac5057a71c65315271c4364f485fabec6fd318a Mon Sep 17 00:00:00 2001 From: Kim Da Eun Date: Thu, 3 Oct 2024 16:43:59 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EB=B0=B0=EB=84=88=20ssr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 6 ++++ ssr/server/constants.js | 22 +++++++++++++++ ssr/server/movies.js | 40 ++++++++++++++++++++++++++ ssr/server/routes/index.js | 57 ++++++++++++++++++++++++++++++++------ ssr/views/index.html | 17 +----------- 5 files changed, 117 insertions(+), 25 deletions(-) create mode 100644 package-lock.json create mode 100644 ssr/server/constants.js create mode 100644 ssr/server/movies.js diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1889713 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "ssr-basecamp", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/ssr/server/constants.js b/ssr/server/constants.js new file mode 100644 index 0000000..9f3cea6 --- /dev/null +++ b/ssr/server/constants.js @@ -0,0 +1,22 @@ +export const BASE_URL = 'https://api.themoviedb.org/3/movie'; + +export const TMDB_THUMBNAIL_URL = + 'https://media.themoviedb.org/t/p/w440_and_h660_face/'; +export const TMDB_ORIGINAL_URL = 'https://image.tmdb.org/t/p/original/'; +export const TMDB_BANNER_URL = + 'https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/'; +export const TMDB_MOVIE_LISTS = { + popular: BASE_URL + '/popular?language=ko-KR&page=1', + nowPlaying: BASE_URL + '/now_playing?language=ko-KR&page=1', + topRated: BASE_URL + '/top_rated?language=ko-KR&page=1', + upcoming: BASE_URL + '/upcoming?language=ko-KR&page=1', +}; +export const TMDB_MOVIE_DETAIL_URL = 'https://api.themoviedb.org/3/movie/'; + +export const FETCH_OPTIONS = { + method: 'GET', + headers: { + accept: 'application/json', + Authorization: 'Bearer ' + process.env.TMDB_TOKEN, + }, +}; diff --git a/ssr/server/movies.js b/ssr/server/movies.js new file mode 100644 index 0000000..94a5bb7 --- /dev/null +++ b/ssr/server/movies.js @@ -0,0 +1,40 @@ +import { FETCH_OPTIONS, TMDB_MOVIE_LISTS } from './constants.js'; + +const loadMovies = async (url) => { + try { + const response = await fetch(url, FETCH_OPTIONS); + + if (!response.ok) { + throw new Error(`Failed to fetch movies: ${response.status}`); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error(error); + } +}; + +const fetchMovies = { + popular: async () => { + const data = await loadMovies(TMDB_MOVIE_LISTS.popular); + return data; + }, + + nowPlaying: async () => { + const data = await loadMovies(TMDB_MOVIE_LISTS.nowPlaying); + return data; + }, + + upcoming: async () => { + const data = await loadMovies(TMDB_MOVIE_LISTS.upcoming); + return data; + }, + + topRated: async () => { + const data = await loadMovies(TMDB_MOVIE_LISTS.topRated); + return data; + }, +}; + +export default fetchMovies; diff --git a/ssr/server/routes/index.js b/ssr/server/routes/index.js index 84d32f2..1c57fe3 100644 --- a/ssr/server/routes/index.js +++ b/ssr/server/routes/index.js @@ -1,19 +1,58 @@ -import { Router } from "express"; -import fs from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; +import { Router } from 'express'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +import fetchMovies from '../movies.js'; +import { TMDB_BANNER_URL, TMDB_THUMBNAIL_URL } from '../constants.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const router = Router(); -router.get("/", (_, res) => { - const templatePath = path.join(__dirname, "../../views", "index.html"); - const moviesHTML = "

들어갈 본문 작성

"; +const getBannerHTML = async () => { + const topRatedMovies = await fetchMovies.topRated(); + const { + vote_average: rate, + title, + backdrop_path: backdropPath, + } = topRatedMovies.results[0]; + + return /*html*/ ` +
+
+ +
+

+ MovieList +

+
+
+ + ${rate.toFixed(1)} +
+
${title}
+ +
+
+
+
+ `; +}; +const placeholders = { + banner: '', +}; +router.get('/', async (_, res) => { + const templatePath = path.join(__dirname, '../../views', 'index.html'); + const bannerHTML = await getBannerHTML(); - const template = fs.readFileSync(templatePath, "utf-8"); - const renderedHTML = template.replace("", moviesHTML); + const template = fs.readFileSync(templatePath, 'utf-8'); + const renderedHTML = template + .replace(placeholders.banner, bannerHTML) res.send(renderedHTML); }); diff --git a/ssr/views/index.html b/ssr/views/index.html index a052396..eb5b578 100644 --- a/ssr/views/index.html +++ b/ssr/views/index.html @@ -13,22 +13,7 @@
-
-
- -
-

MovieList

-
-
- - ${bestMovie.rate} -
-
${bestMovie.title}
- -
-
-
-
+
  • From 21ec3d9f8581f746a269f7720e6455f02cab7c7b Mon Sep 17 00:00:00 2001 From: Kim Da Eun Date: Thu, 3 Oct 2024 16:44:26 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EC=98=81=ED=99=94=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20ssr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ssr/server/routes/index.js | 38 ++++++++++++++++++++++++++++++++++++++ ssr/views/index.html | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/ssr/server/routes/index.js b/ssr/server/routes/index.js index 1c57fe3..d00027d 100644 --- a/ssr/server/routes/index.js +++ b/ssr/server/routes/index.js @@ -43,16 +43,54 @@ const getBannerHTML = async () => { `; }; + +const getMoviesHTML = async (category) => { + const movies = await fetchMovies[category](); + return /*html*/ ` + ${movies.results + .map( + ({ + id, + title, + vote_average: rate, + poster_path: thumbnailPath, + }) => /*html*/ ` +
  • + +
    + ${title} +
    +

    + + ${rate.toFixed(1)} +

    + ${title} +
    +
    +
    +
  • + ` + ) + .join('\n')} + `; +}; + const placeholders = { banner: '', + movies: '', }; + router.get('/', async (_, res) => { const templatePath = path.join(__dirname, '../../views', 'index.html'); const bannerHTML = await getBannerHTML(); + const moviesHTML = await getMoviesHTML('popular'); const template = fs.readFileSync(templatePath, 'utf-8'); const renderedHTML = template .replace(placeholders.banner, bannerHTML) + .replace(placeholders.movies, moviesHTML); res.send(renderedHTML); }); diff --git a/ssr/views/index.html b/ssr/views/index.html index eb5b578..fadc64a 100644 --- a/ssr/views/index.html +++ b/ssr/views/index.html @@ -49,7 +49,7 @@

    상영 예정

    지금 인기 있는 영화

      - +
    From 8e4680d817bf618508b72f4b5f45e5c6f63711f7 Mon Sep 17 00:00:00 2001 From: Kim Da Eun Date: Thu, 3 Oct 2024 17:20:53 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EC=83=81=EC=98=81=20=EC=A4=91,=20?= =?UTF-8?q?=EC=9D=B8=EA=B8=B0=EC=88=9C,=20=ED=8F=89=EC=A0=90=EC=88=9C,=20?= =?UTF-8?q?=EC=83=81=EC=98=81=20=EC=98=88=EC=A0=95=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ssr/server/routes/index.js | 41 +++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/ssr/server/routes/index.js b/ssr/server/routes/index.js index d00027d..4a5a351 100644 --- a/ssr/server/routes/index.js +++ b/ssr/server/routes/index.js @@ -11,13 +11,8 @@ const __dirname = path.dirname(__filename); const router = Router(); -const getBannerHTML = async () => { - const topRatedMovies = await fetchMovies.topRated(); - const { - vote_average: rate, - title, - backdrop_path: backdropPath, - } = topRatedMovies.results[0]; +const getBannerHTML = async (movie) => { + const { vote_average: rate, title, backdrop_path: backdropPath } = movie; return /*html*/ `
    @@ -44,8 +39,7 @@ const getBannerHTML = async () => { `; }; -const getMoviesHTML = async (category) => { - const movies = await fetchMovies[category](); +const getMoviesHTML = async (movies) => { return /*html*/ ` ${movies.results .map( @@ -82,17 +76,40 @@ const placeholders = { movies: '', }; -router.get('/', async (_, res) => { +const renderPage = async (category, res) => { + const movies = await fetchMovies[category](); + const templatePath = path.join(__dirname, '../../views', 'index.html'); - const bannerHTML = await getBannerHTML(); - const moviesHTML = await getMoviesHTML('popular'); + const bannerHTML = await getBannerHTML(movies.results[0]); + const moviesHTML = await getMoviesHTML(movies); const template = fs.readFileSync(templatePath, 'utf-8'); + const renderedHTML = template .replace(placeholders.banner, bannerHTML) .replace(placeholders.movies, moviesHTML); res.send(renderedHTML); +}; + +router.get('/', async (_, res) => { + await renderPage('nowPlaying', res); +}); + +router.get('/now-playing', async (_, res) => { + await renderPage('nowPlaying', res); +}); + +router.get('/popular', async (_, res) => { + await renderPage('popular', res); +}); + +router.get('/upcoming', async (_, res) => { + await renderPage('upcoming', res); +}); + +router.get('/top-rated', async (_, res) => { + await renderPage('topRated', res); }); export default router; From 315fb3cc10f10925d28c9a0d8e62e0ea8f892842 Mon Sep 17 00:00:00 2001 From: Kim Da Eun Date: Thu, 3 Oct 2024 20:11:51 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=EC=98=81=ED=99=94=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=EC=A0=95=EB=B3=B4=20=EB=AA=A8=EB=8B=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ssr/public/scripts/modal.js | 20 +++++++ ssr/server/movies.js | 11 +++- ssr/server/routes/index.js | 96 +++++++++--------------------- ssr/server/templates/movie.js | 108 ++++++++++++++++++++++++++++++++++ ssr/views/index.html | 2 +- 5 files changed, 166 insertions(+), 71 deletions(-) create mode 100644 ssr/public/scripts/modal.js create mode 100644 ssr/server/templates/movie.js diff --git a/ssr/public/scripts/modal.js b/ssr/public/scripts/modal.js new file mode 100644 index 0000000..241e921 --- /dev/null +++ b/ssr/public/scripts/modal.js @@ -0,0 +1,20 @@ +document.addEventListener('DOMContentLoaded', () => { + const modalBackground = document.querySelector('.modal-background'); + const modalCloseButton = document.querySelector('.close-modal'); + + if (modalCloseButton) { + modalCloseButton.addEventListener('click', () => { + if (modalBackground) { + modalBackground.remove(); + } + }); + } + + if (modalBackground) { + modalBackground.addEventListener('click', (event) => { + if (event.target === modalBackground) { + modalBackground.remove(); + } + }); + } +}); diff --git a/ssr/server/movies.js b/ssr/server/movies.js index 94a5bb7..7d4bb1c 100644 --- a/ssr/server/movies.js +++ b/ssr/server/movies.js @@ -1,4 +1,8 @@ -import { FETCH_OPTIONS, TMDB_MOVIE_LISTS } from './constants.js'; +import { + FETCH_OPTIONS, + TMDB_MOVIE_DETAIL_URL, + TMDB_MOVIE_LISTS, +} from './constants.js'; const loadMovies = async (url) => { try { @@ -35,6 +39,11 @@ const fetchMovies = { const data = await loadMovies(TMDB_MOVIE_LISTS.topRated); return data; }, + + detail: async (id) => { + const data = await loadMovies(`${TMDB_MOVIE_DETAIL_URL}${id}`); + return data; + }, }; export default fetchMovies; diff --git a/ssr/server/routes/index.js b/ssr/server/routes/index.js index 4a5a351..74641c0 100644 --- a/ssr/server/routes/index.js +++ b/ssr/server/routes/index.js @@ -4,112 +4,70 @@ import path from 'path'; import { fileURLToPath } from 'url'; import fetchMovies from '../movies.js'; -import { TMDB_BANNER_URL, TMDB_THUMBNAIL_URL } from '../constants.js'; +import { + getBannerHTML, + getModalHTML, + getMoviesHTML, +} from '../templates/movie.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const router = Router(); -const getBannerHTML = async (movie) => { - const { vote_average: rate, title, backdrop_path: backdropPath } = movie; - - return /*html*/ ` -
    -
    - -
    -

    - MovieList -

    -
    -
    - - ${rate.toFixed(1)} -
    -
    ${title}
    - -
    -
    -
    -
    - `; -}; - -const getMoviesHTML = async (movies) => { - return /*html*/ ` - ${movies.results - .map( - ({ - id, - title, - vote_average: rate, - poster_path: thumbnailPath, - }) => /*html*/ ` -
  • - -
    - ${title} -
    -

    - - ${rate.toFixed(1)} -

    - ${title} -
    -
    -
    -
  • - ` - ) - .join('\n')} - `; -}; - const placeholders = { banner: '', movies: '', + modal: '', }; -const renderPage = async (category, res) => { +const renderMoviePage = async ({ + res, + category = 'nowPlaying', + movieId = null, +}) => { + const templatePath = path.join(__dirname, '../../views', 'index.html'); + const movies = await fetchMovies[category](); + const movieDetail = movieId ? await fetchMovies.detail(movieId) : null; - const templatePath = path.join(__dirname, '../../views', 'index.html'); const bannerHTML = await getBannerHTML(movies.results[0]); const moviesHTML = await getMoviesHTML(movies); + const modalHTML = movieDetail ? await getModalHTML(movieDetail) : ''; const template = fs.readFileSync(templatePath, 'utf-8'); const renderedHTML = template .replace(placeholders.banner, bannerHTML) - .replace(placeholders.movies, moviesHTML); + .replace(placeholders.movies, moviesHTML) + .replace(placeholders.modal, modalHTML); res.send(renderedHTML); }; router.get('/', async (_, res) => { - await renderPage('nowPlaying', res); + await renderMoviePage({ res }); }); router.get('/now-playing', async (_, res) => { - await renderPage('nowPlaying', res); + await renderMoviePage({ category: 'nowPlaying', res }); }); router.get('/popular', async (_, res) => { - await renderPage('popular', res); + await renderMoviePage({ category: 'popular', res }); }); router.get('/upcoming', async (_, res) => { - await renderPage('upcoming', res); + await renderMoviePage({ category: 'upcoming', res }); }); router.get('/top-rated', async (_, res) => { - await renderPage('topRated', res); + await renderMoviePage({ category: 'topRated', res }); +}); + +router.get('/detail/:id', async (req, res) => { + const { id } = req.params; + await renderMoviePage({ res, movieId: id }); }); export default router; diff --git a/ssr/server/templates/movie.js b/ssr/server/templates/movie.js new file mode 100644 index 0000000..287baaa --- /dev/null +++ b/ssr/server/templates/movie.js @@ -0,0 +1,108 @@ +import { + TMDB_BANNER_URL, + TMDB_ORIGINAL_URL, + TMDB_THUMBNAIL_URL, +} from '../constants.js'; + +export const getBannerHTML = async (movie) => { + const { vote_average: rate, title, backdrop_path: backdropPath } = movie; + + return /*html*/ ` +
    +
    + +
    +

    + MovieList +

    +
    +
    + + ${rate.toFixed(1)} +
    +
    ${title}
    + +
    +
    +
    +
    + `; +}; + +export const getMoviesHTML = async (movies) => { + return /*html*/ ` + ${movies.results + .map( + ({ + id, + title, + vote_average: rate, + poster_path: thumbnailPath, + }) => /*html*/ ` +
  • + +
    + ${title} +
    +

    + + ${rate.toFixed(1)} +

    + ${title} +
    +
    +
    +
  • + ` + ) + .join('\n')} + `; +}; + +export const getModalHTML = async (movieDetail) => { + const { + poster_path: posterPath, + title, + release_date: releaseDate, + genres, + vote_average: rate, + overview: description, + } = movieDetail; + return ` + + + + `; +}; diff --git a/ssr/views/index.html b/ssr/views/index.html index fadc64a..68bc93b 100644 --- a/ssr/views/index.html +++ b/ssr/views/index.html @@ -60,6 +60,6 @@

    지금 인기 있는 영화

- + From 2629f277c5f6c2cc5e26ae2de6bd59179cf67bd1 Mon Sep 17 00:00:00 2001 From: Kim Da Eun Date: Thu, 3 Oct 2024 20:46:55 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20tab=20selected=20=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ssr/server/{ => api}/constants.js | 0 ssr/server/{ => api}/movies.js | 0 ssr/server/routes/index.js | 6 +++- ssr/server/templates/movie.js | 49 +++++++++++++++++++++++-------- ssr/views/index.html | 29 +----------------- 5 files changed, 42 insertions(+), 42 deletions(-) rename ssr/server/{ => api}/constants.js (100%) rename ssr/server/{ => api}/movies.js (100%) diff --git a/ssr/server/constants.js b/ssr/server/api/constants.js similarity index 100% rename from ssr/server/constants.js rename to ssr/server/api/constants.js diff --git a/ssr/server/movies.js b/ssr/server/api/movies.js similarity index 100% rename from ssr/server/movies.js rename to ssr/server/api/movies.js diff --git a/ssr/server/routes/index.js b/ssr/server/routes/index.js index 74641c0..0a34180 100644 --- a/ssr/server/routes/index.js +++ b/ssr/server/routes/index.js @@ -3,11 +3,12 @@ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; -import fetchMovies from '../movies.js'; +import fetchMovies from '../api/movies.js'; import { getBannerHTML, getModalHTML, getMoviesHTML, + getTabsHTML, } from '../templates/movie.js'; const __filename = fileURLToPath(import.meta.url); @@ -19,6 +20,7 @@ const placeholders = { banner: '', movies: '', modal: '', + tabs: '', }; const renderMoviePage = async ({ @@ -32,6 +34,7 @@ const renderMoviePage = async ({ const movieDetail = movieId ? await fetchMovies.detail(movieId) : null; const bannerHTML = await getBannerHTML(movies.results[0]); + const tabsHTML = getTabsHTML(category); const moviesHTML = await getMoviesHTML(movies); const modalHTML = movieDetail ? await getModalHTML(movieDetail) : ''; @@ -39,6 +42,7 @@ const renderMoviePage = async ({ const renderedHTML = template .replace(placeholders.banner, bannerHTML) + .replace(placeholders.tabs, tabsHTML) .replace(placeholders.movies, moviesHTML) .replace(placeholders.modal, modalHTML); diff --git a/ssr/server/templates/movie.js b/ssr/server/templates/movie.js index 287baaa..54c2e0b 100644 --- a/ssr/server/templates/movie.js +++ b/ssr/server/templates/movie.js @@ -2,7 +2,7 @@ import { TMDB_BANNER_URL, TMDB_ORIGINAL_URL, TMDB_THUMBNAIL_URL, -} from '../constants.js'; +} from '../api/constants.js'; export const getBannerHTML = async (movie) => { const { vote_average: rate, title, backdrop_path: backdropPath } = movie; @@ -29,19 +29,43 @@ export const getBannerHTML = async (movie) => {
+ + `; }; +export const getTabsHTML = (category) => { + const tabs = { + nowPlaying: { href: '/now-playing', label: '상영 중' }, + popular: { href: '/popular', label: '인기' }, + upcoming: { href: '/upcoming', label: '개봉 예정' }, + topRated: { href: '/top-rated', label: '평점 높은 순' }, + }; + + return Object.entries(tabs) + .map(([menu, { href, label }]) => { + return /*html*/ ` +
  • + +
    +

    ${label}

    +
    +
    +
  • + `; + }) + .join('\n'); +}; + export const getMoviesHTML = async (movies) => { - return /*html*/ ` - ${movies.results - .map( - ({ - id, - title, - vote_average: rate, - poster_path: thumbnailPath, - }) => /*html*/ ` + return movies.results + .map( + ({ + id, + title, + vote_average: rate, + poster_path: thumbnailPath, + }) => /*html*/ `
  • ` - ) - .join('\n')} - `; + ) + .join('\n'); }; export const getModalHTML = async (movieDetail) => { diff --git a/ssr/views/index.html b/ssr/views/index.html index 68bc93b..940d689 100644 --- a/ssr/views/index.html +++ b/ssr/views/index.html @@ -16,34 +16,7 @@