diff --git a/ssr/package-lock.json b/ssr/package-lock.json index a73fb42..e3e83dd 100644 --- a/ssr/package-lock.json +++ b/ssr/package-lock.json @@ -12,6 +12,7 @@ "node-fetch": "^3.3.2" }, "devDependencies": { + "cross-env": "^7.0.3", "dotenv": "^16.0.0", "nodemon": "^3.1.6" } @@ -197,6 +198,38 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -625,6 +658,12 @@ "node": ">=0.12.0" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -829,6 +868,15 @@ "node": ">= 0.8" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-to-regexp": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", @@ -1019,6 +1067,27 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -1146,6 +1215,21 @@ "engines": { "node": ">= 8" } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } } } } diff --git a/ssr/package.json b/ssr/package.json index db1a534..ff8de99 100644 --- a/ssr/package.json +++ b/ssr/package.json @@ -4,8 +4,8 @@ "description": "SSR 렌더링으로 영화 목록 불러오기", "main": "server/index.js", "scripts": { - "start": "NODE_TLS_REJECT_UNAUTHORIZED=0 node server/index.js", - "dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 nodemon server/index.js --watch" + "start": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 node server/index.js", + "dev": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 nodemon server/index.js --watch" }, "type": "module", "dependencies": { @@ -13,7 +13,8 @@ "node-fetch": "^3.3.2" }, "devDependencies": { - "nodemon": "^3.1.6", - "dotenv": "^16.0.0" + "cross-env": "^7.0.3", + "dotenv": "^16.0.0", + "nodemon": "^3.1.6" } } diff --git a/ssr/server/index.js b/ssr/server/index.js index 09efd9a..8a49355 100644 --- a/ssr/server/index.js +++ b/ssr/server/index.js @@ -1,9 +1,9 @@ -import "./config.js"; -import express from "express"; -import path from "path"; -import { fileURLToPath } from "url"; +import './config.js'; +import express from 'express'; +import path from 'path'; +import { fileURLToPath } from 'url'; -import movieRouter from "./routes/index.js"; +import movieRouter from './routes/index.js'; // import membersRouter from "./routes/members.js"; // 본 미션 참고를 위한 코드이며 사전 미션에서는 사용하지 않습니다. const app = express(); @@ -12,9 +12,9 @@ const PORT = 3000; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -app.use("/assets", express.static(path.join(__dirname, "../public"))); +app.use('/assets', express.static(path.join(__dirname, '../public'))); -app.use("/", movieRouter); +app.use('/', movieRouter); // app.use("/members", membersRouter); // 본 미션 참고를 위한 코드이며 사전 미션에서는 사용하지 않습니다. // Start server diff --git a/ssr/server/routes/index.js b/ssr/server/routes/index.js index 84d32f2..e72fbcf 100644 --- a/ssr/server/routes/index.js +++ b/ssr/server/routes/index.js @@ -1,21 +1,115 @@ -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 { fetchMovieDetail, fetchMovies } from '../../src/api/movie.js'; +import { renderMovieItemPage } from '../../src/templates/renderMovieItemPage.js'; +import { TMDB_MOVIE_LISTS } from '../../src/constants/constant.js'; +import { renderModal } from '../../src/templates/renderModal.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 = "

들어갈 본문 작성

"; +router.get('/', async (_, res) => { + try { + const movies = await fetchMovies(TMDB_MOVIE_LISTS.POPULAR); // Fetch movie data from API - const template = fs.readFileSync(templatePath, "utf-8"); - const renderedHTML = template.replace("", moviesHTML); + const templatePath = path.join(__dirname, '../../views', 'index.html'); + const template = fs.readFileSync(templatePath, 'utf-8'); - res.send(renderedHTML); + const renderedHTML = renderMovieItemPage(movies); + + res.send(renderedHTML); + } catch (error) { + console.error('Error fetching movies:', error); + res.status(500).send('Error rendering the page'); + } +}); + +router.get('/now-playing', async (_, res) => { + try { + const movies = await fetchMovies(TMDB_MOVIE_LISTS.NOW_PLAYING); + + const templatePath = path.join(__dirname, '../../views', 'index.html'); + const template = fs.readFileSync(templatePath, 'utf-8'); + + const renderedHTML = renderMovieItemPage(movies); + + res.send(renderedHTML); + } catch (error) { + console.error('Error fetching movies:', error); + res.status(500).send('Error rendering the page'); + } +}); + +router.get('/popular', async (_, res) => { + try { + const movies = await fetchMovies(TMDB_MOVIE_LISTS.POPULAR); + + const templatePath = path.join(__dirname, '../../views', 'index.html'); + const template = fs.readFileSync(templatePath, 'utf-8'); + + const renderedHTML = renderMovieItemPage(movies); + + res.send(renderedHTML); + } catch (error) { + console.error('Error fetching movies:', error); + res.status(500).send('Error rendering the page'); + } +}); + +router.get('/top-rated', async (_, res) => { + try { + const movies = await fetchMovies(TMDB_MOVIE_LISTS.TOP_RATED); + + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + + const templatePath = path.join(__dirname, '../../views', 'index.html'); + const template = fs.readFileSync(templatePath, 'utf-8'); + + const renderedHTML = renderMovieItemPage(movies); + + res.send(renderedHTML); + } catch (error) { + console.error('Error fetching movies:', error); + res.status(500).send('Error rendering the page'); + } +}); + +router.get('/upcoming', async (_, res) => { + try { + const movies = await fetchMovies(TMDB_MOVIE_LISTS.UPCOMING); + + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + + const templatePath = path.join(__dirname, '../../views', 'index.html'); + const template = fs.readFileSync(templatePath, 'utf-8'); + + const renderedHTML = renderMovieItemPage(movies); + + res.send(renderedHTML); + } catch (error) { + console.error('Error fetching movies:', error); + res.status(500).send('Error rendering the page'); + } +}); + +router.get('/detail/:id', async (req, res) => { + try { + const movieId = req.params.id; + const moviesData = await fetchMovies(TMDB_MOVIE_LISTS.NOW_PLAYING); + const movieDetail = await fetchMovieDetail(movieId); + const modalHTML = renderModal(movieDetail, moviesData); + + res.send(modalHTML); + } catch (error) { + console.error('Error fetching movie details:', error); + res.status(500).send('Error fetching movie details'); + } }); export default router; diff --git a/ssr/src/api/movie.js b/ssr/src/api/movie.js new file mode 100644 index 0000000..34b04f6 --- /dev/null +++ b/ssr/src/api/movie.js @@ -0,0 +1,26 @@ +import { FETCH_OPTIONS, TMDB_MOVIE_DETAIL_URL } from '../constants/constant.js'; + +export const fetchMovies = async (url) => { + const response = await fetch(url, FETCH_OPTIONS); + return await response.json(); // Parse and return the response JSON +}; + +export const fetchMovieDetail = async (movieId) => { + const url = `${TMDB_MOVIE_DETAIL_URL}${movieId}?language=ko-KR`; + + const response = await fetch(url, FETCH_OPTIONS); + if (!response.ok) { + throw new Error('Failed to fetch movie details'); + } + + const data = await response.json(); + return { + title: data.title, + releaseYear: data.release_date.split('-')[0], + genres: data.genres.map((genre) => genre.name), + description: data.overview, + poster_path: data.poster_path + ? `https://image.tmdb.org/t/p/original${data.poster_path}` + : '', + }; +}; diff --git a/ssr/src/constants/constant.js b/ssr/src/constants/constant.js new file mode 100644 index 0000000..8b31402 --- /dev/null +++ b/ssr/src/constants/constant.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', + NOW_PLAYING: BASE_URL + '/now_playing?language=ko-KR&page=1', + TOP_RATED: 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/src/templates/renderModal.js b/ssr/src/templates/renderModal.js new file mode 100644 index 0000000..68a06ac --- /dev/null +++ b/ssr/src/templates/renderModal.js @@ -0,0 +1,42 @@ +import { renderMovieItemPage } from './renderMovieItemPage.js'; + +export const renderModal = (movieDetail, moviesData) => { + const moviesPageTemplate = renderMovieItemPage(moviesData); + return moviesPageTemplate.replace( + '', + /*html*/ ` + + + + `, + ); +}; diff --git a/ssr/src/templates/renderMovieItemPage.js b/ssr/src/templates/renderMovieItemPage.js new file mode 100644 index 0000000..db06e63 --- /dev/null +++ b/ssr/src/templates/renderMovieItemPage.js @@ -0,0 +1,28 @@ +import fs from 'fs'; +import path from 'path'; +import { renderMovieItems } from './renderMovieItems.js'; // Function to render individual movie items +import { fileURLToPath } from 'url'; + +export const renderMovieItemPage = (moviesData) => { + const movieItemsHTML = renderMovieItems(moviesData.results); // Assuming 'results' contains the array of movies + const bestMovie = moviesData.results[0]; // First movie is used as the "best" movie + + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + + const templatePath = path.join(__dirname, '../../views', 'index.html'); + let template = fs.readFileSync(templatePath, 'utf-8'); + + template = template.replace( + '', + movieItemsHTML, + ); + template = template.replace( + '${background-container}', + `https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${bestMovie.backdrop_path}`, + ); + template = template.replace('${bestMovie.rate}', bestMovie.vote_average); + template = template.replace('${bestMovie.title}', bestMovie.title); + + return template; +}; diff --git a/ssr/src/templates/renderMovieItems.js b/ssr/src/templates/renderMovieItems.js new file mode 100644 index 0000000..2c31027 --- /dev/null +++ b/ssr/src/templates/renderMovieItems.js @@ -0,0 +1,23 @@ +export const renderMovieItems = (movieItems) => { + return movieItems + .map( + ({ id, title, poster_path, vote_average }) => ` +
  • + +
    + ${title} +
    +

    ${vote_average}

    + ${title} +
    +
    +
    +
  • + `, + ) + .join(''); +}; diff --git a/ssr/views/index.html b/ssr/views/index.html index a052396..77d96a2 100644 --- a/ssr/views/index.html +++ b/ssr/views/index.html @@ -14,10 +14,15 @@
    -
    +
    -

    MovieList

    +

    + MovieList +

    @@ -76,5 +81,34 @@

    지금 인기 있는 영화

    +