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/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/api/constants.js b/ssr/server/api/constants.js new file mode 100644 index 0000000..9f3cea6 --- /dev/null +++ b/ssr/server/api/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/api/movies.js b/ssr/server/api/movies.js new file mode 100644 index 0000000..7d4bb1c --- /dev/null +++ b/ssr/server/api/movies.js @@ -0,0 +1,49 @@ +import { + FETCH_OPTIONS, + TMDB_MOVIE_DETAIL_URL, + 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; + }, + + 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 84d32f2..0a34180 100644 --- a/ssr/server/routes/index.js +++ b/ssr/server/routes/index.js @@ -1,21 +1,77 @@ -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 '../api/movies.js'; +import { + getBannerHTML, + getModalHTML, + getMoviesHTML, + getTabsHTML, +} from '../templates/movie.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 placeholders = { + banner: '', + movies: '', + modal: '', + tabs: '', +}; + +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 bannerHTML = await getBannerHTML(movies.results[0]); + const tabsHTML = getTabsHTML(category); + const moviesHTML = await getMoviesHTML(movies); + const modalHTML = movieDetail ? await getModalHTML(movieDetail) : ''; - 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) + .replace(placeholders.tabs, tabsHTML) + .replace(placeholders.movies, moviesHTML) + .replace(placeholders.modal, modalHTML); res.send(renderedHTML); +}; + +router.get('/', async (_, res) => { + await renderMoviePage({ res }); +}); + +router.get('/now-playing', async (_, res) => { + await renderMoviePage({ category: 'nowPlaying', res }); +}); + +router.get('/popular', async (_, res) => { + await renderMoviePage({ category: 'popular', res }); +}); + +router.get('/upcoming', async (_, res) => { + await renderMoviePage({ category: 'upcoming', res }); +}); + +router.get('/top-rated', async (_, 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..54c2e0b --- /dev/null +++ b/ssr/server/templates/movie.js @@ -0,0 +1,131 @@ +import { + TMDB_BANNER_URL, + TMDB_ORIGINAL_URL, + TMDB_THUMBNAIL_URL, +} from '../api/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 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 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 a052396..940d689 100644 --- a/ssr/views/index.html +++ b/ssr/views/index.html @@ -13,58 +13,16 @@
    -
    -
    - -
    -

    MovieList

    -
    -
    - - ${bestMovie.rate} -
    -
    ${bestMovie.title}
    - -
    -
    -
    -
    +

    지금 인기 있는 영화

      - +
    @@ -75,6 +33,6 @@

    지금 인기 있는 영화

    - +