From 1cbba2b06ac41d08a5ccaead76ed6c68f47cd6a6 Mon Sep 17 00:00:00 2001 From: ImxYJL Date: Thu, 3 Oct 2024 17:14:18 +0900 Subject: [PATCH 1/4] =?UTF-8?q?chore:=20=ED=99=98=EA=B2=BD=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ssr/package-lock.json | 84 +++++++++++++++++++++++++++++++++++++++++++ ssr/package.json | 9 ++--- 2 files changed, 89 insertions(+), 4 deletions(-) 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" } } From d3db984ed28677bb29c280babab5e71f044fd973 Mon Sep 17 00:00:00 2001 From: ImxYJL Date: Thu, 3 Oct 2024 17:15:00 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=EC=82=AC=EC=A0=84=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ssr/server/controllers/moviesController.js | 139 +++++++++++++++++++++ ssr/server/routes/index.js | 21 +--- ssr/server/routes/movies.js | 23 ++++ ssr/src/constants.js | 22 ++++ ssr/views/index.html | 13 +- 5 files changed, 197 insertions(+), 21 deletions(-) create mode 100644 ssr/server/controllers/moviesController.js create mode 100644 ssr/server/routes/movies.js create mode 100644 ssr/src/constants.js diff --git a/ssr/server/controllers/moviesController.js b/ssr/server/controllers/moviesController.js new file mode 100644 index 0000000..a5a824b --- /dev/null +++ b/ssr/server/controllers/moviesController.js @@ -0,0 +1,139 @@ +// 영화 데이터와 관련된 로직 처리 +import fetch from "node-fetch"; +import { + TMDB_MOVIE_LISTS, + FETCH_OPTIONS, + TMDB_BANNER_URL, + TMDB_THUMBNAIL_URL, +} from "../../src/constants.js"; +import path from "path"; +import fs from "fs"; +import { fileURLToPath } from "url"; + +// 현재 파일 경로와 디렉터리 경로 설정 +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const getFullThumbNailURL = (thumbnailUrl) => { + return TMDB_THUMBNAIL_URL + "/" + thumbnailUrl; +}; + +const getBestMovie = (movies) => { + return movies.reduce( + (best, movie) => (movie.vote_average > best.vote_average ? movie : best), + movies[0] + ); +}; + +const getMovieListInnerHTML = (movie) => { + return ` +
+ + + +
+

+ + ${(Math.round(movie.vote_average * 10) / 10).toFixed( + 1 + )} +

+ ${movie.title} +
+
+ `; +}; + +const getMovieDetailModalInnerHTML = (movieDetail) => { + return ` + + + `; +}; + +export const renderMoviesPage = async (req, res, listType, movieId = null) => { + try { + const response = await fetch(TMDB_MOVIE_LISTS[listType], FETCH_OPTIONS); + const moviesData = await response.json(); + + // 최고 평점 영화 추출 + const bestMovie = getBestMovie(moviesData.results); + + const bestMovieBanner = bestMovie + ? TMDB_BANNER_URL + bestMovie.backdrop_path + : ""; + const bestMovieTitle = bestMovie ? bestMovie.title : "No title"; + const bestMovieRate = bestMovie ? bestMovie.vote_average.toFixed(1) : ""; + + // 영화 목록 데이터를 HTML로 변환 + const moviesHTML = moviesData.results + .map((movie) => getMovieListInnerHTML(movie)) + .join(""); + + let modalHTML = ""; + + // 만약 movieId가 주어졌다면 영화 상세 정보로 모달 생성 + if (movieId) { + const movieResponse = await fetch( + `https://api.themoviedb.org/3/movie/${movieId}?api_key=${process.env.TMDB_TOKEN}`, + FETCH_OPTIONS + ); + const movieDetail = await movieResponse.json(); + + modalHTML = getMovieDetailModalInnerHTML(movieDetail); + } + + // 템플릿 파일 읽기 + const templatePath = path.join(__dirname, "../../views", "index.html"); + const template = fs.readFileSync(templatePath, "utf-8"); + + // 템플릿에 영화 데이터 및 모달 데이터 삽입 + let renderedHTML = template + .replace("${background-container}", bestMovieBanner) + .replace("${MOVIE_ITEMS_PLACEHOLDER}", moviesHTML) + .replace("${bestMovie.title}", bestMovieTitle) + .replace("${bestMovie.rate}", bestMovieRate) + .replace("${MODAL_AREA}", modalHTML); + + res.send(renderedHTML); + } catch (error) { + console.error("Error fetching movies:", error); + res.status(500).send("Server Error"); + } +}; diff --git a/ssr/server/routes/index.js b/ssr/server/routes/index.js index 84d32f2..5a43f15 100644 --- a/ssr/server/routes/index.js +++ b/ssr/server/routes/index.js @@ -1,21 +1,8 @@ -import { Router } from "express"; -import fs from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; +import express from "express"; +import moviesRouter from "./movies.js"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +const router = express.Router(); -const router = Router(); - -router.get("/", (_, res) => { - const templatePath = path.join(__dirname, "../../views", "index.html"); - const moviesHTML = "

들어갈 본문 작성

"; - - const template = fs.readFileSync(templatePath, "utf-8"); - const renderedHTML = template.replace("", moviesHTML); - - res.send(renderedHTML); -}); +router.use("/", moviesRouter); export default router; diff --git a/ssr/server/routes/movies.js b/ssr/server/routes/movies.js new file mode 100644 index 0000000..b451d96 --- /dev/null +++ b/ssr/server/routes/movies.js @@ -0,0 +1,23 @@ +import express from "express"; +import { renderMoviesPage } from "../controllers/moviesController.js"; + +// 각각의 경로에서 영화 데이터를 가져와 렌더링하는 라우트 +const router = express.Router(); + +router.get("/", (req, res) => { + renderMoviesPage(req, res, "POPULAR"); +}); + +router.get("/now-playing", (req, res) => + renderMoviesPage(req, res, "NOW_PLAYING") +); + +router.get("/popular", (req, res) => renderMoviesPage(req, res, "POPULAR")); +router.get("/top-rated", (req, res) => renderMoviesPage(req, res, "TOP_RATED")); +router.get("/upcoming", (req, res) => renderMoviesPage(req, res, "UPCOMING")); +router.get("/detail/:id", (req, res) => { + const movieId = req.params.id; + renderMoviesPage(req, res, "POPULAR", movieId); // 영화 목록과 모달 함께 렌더링 +}); + +export default router; diff --git a/ssr/src/constants.js b/ssr/src/constants.js new file mode 100644 index 0000000..b26b9a7 --- /dev/null +++ b/ssr/src/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", + 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/views/index.html b/ssr/views/index.html index a052396..140021b 100644 --- a/ssr/views/index.html +++ b/ssr/views/index.html @@ -14,10 +14,15 @@
-
+
-

MovieList

+

+ MovieList +

@@ -64,7 +69,7 @@

상영 예정

지금 인기 있는 영화

    - + ${MOVIE_ITEMS_PLACEHOLDER}
@@ -75,6 +80,6 @@

지금 인기 있는 영화

- + ${MODAL_AREA} From bafdd5d9d19ac64a0df8f1298951a1aba507ddb4 Mon Sep 17 00:00:00 2001 From: ImxYJL Date: Thu, 3 Oct 2024 18:24:51 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EC=84=A0=ED=83=9D=EB=90=9C=20tab?= =?UTF-8?q?=20=EC=8A=A4=ED=83=80=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/controllers/moviesController.js | 18 ++++++++++++++ ssr/src/constants.js | 7 ++++++ ssr/views/index.html | 29 +--------------------- 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/ssr/server/controllers/moviesController.js b/ssr/server/controllers/moviesController.js index a5a824b..590eaf8 100644 --- a/ssr/server/controllers/moviesController.js +++ b/ssr/server/controllers/moviesController.js @@ -5,6 +5,7 @@ import { FETCH_OPTIONS, TMDB_BANNER_URL, TMDB_THUMBNAIL_URL, + TAB_DATA, } from "../../src/constants.js"; import path from "path"; import fs from "fs"; @@ -80,6 +81,7 @@ const getMovieDetailModalInnerHTML = (movieDetail) => { const closeModal = document.getElementById("closeModal"); closeModal.addEventListener("click", () => { document.getElementById("modalBackground").remove(); + document.getElementById("${TAB_DATA[0].title}-tab") history.pushState(null, null, '/'); // URL을 목록 페이지로 변경 }); }); @@ -89,6 +91,21 @@ const getMovieDetailModalInnerHTML = (movieDetail) => { export const renderMoviesPage = async (req, res, listType, movieId = null) => { try { + const currentPath = req.path === "/" ? "/now-playing" : req.path; // 현재 요청된 URL 경로 + + const tabsHTML = TAB_DATA.map((tab) => { + const selectedClass = currentPath === tab.path ? "selected" : ""; + return ` +
  • + +
    +

    ${tab.title}

    +
    +
    +
  • + `; + }).join(""); + const response = await fetch(TMDB_MOVIE_LISTS[listType], FETCH_OPTIONS); const moviesData = await response.json(); @@ -125,6 +142,7 @@ export const renderMoviesPage = async (req, res, listType, movieId = null) => { // 템플릿에 영화 데이터 및 모달 데이터 삽입 let renderedHTML = template + .replace("${TAB_ITEMS_PLACEHOLDER}", tabsHTML) .replace("${background-container}", bestMovieBanner) .replace("${MOVIE_ITEMS_PLACEHOLDER}", moviesHTML) .replace("${bestMovie.title}", bestMovieTitle) diff --git a/ssr/src/constants.js b/ssr/src/constants.js index b26b9a7..17326bd 100644 --- a/ssr/src/constants.js +++ b/ssr/src/constants.js @@ -20,3 +20,10 @@ export const FETCH_OPTIONS = { Authorization: "Bearer " + process.env.TMDB_TOKEN, }, }; + +export const TAB_DATA = [ + { path: "/now-playing", title: "상영 중" }, + { path: "/popular", title: "인기순" }, + { path: "/top-rated", title: "평점순" }, + { path: "/upcoming", title: "상영 예정" }, +]; diff --git a/ssr/views/index.html b/ssr/views/index.html index 140021b..e655122 100644 --- a/ssr/views/index.html +++ b/ssr/views/index.html @@ -36,34 +36,7 @@

    From dcb8525e99aadb2d5ff3efd4bd10f8aa479583e8 Mon Sep 17 00:00:00 2001 From: ImxYJL Date: Thu, 3 Oct 2024 19:19:56 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20=EB=A3=A8=ED=8A=B8=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=EC=97=90=EC=84=9C=20=EB=B3=B4=EC=97=AC=EC=A3=BC?= =?UTF-8?q?=EB=8A=94=20=EC=98=81=ED=99=94=20=EB=AA=A9=EB=A1=9D=20=EA=B8=B0?= =?UTF-8?q?=EC=A4=80=EC=9D=84=20"=EC=83=81=EC=98=81=20=EC=A4=91"=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ssr/server/routes/movies.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ssr/server/routes/movies.js b/ssr/server/routes/movies.js index b451d96..19d530c 100644 --- a/ssr/server/routes/movies.js +++ b/ssr/server/routes/movies.js @@ -5,7 +5,7 @@ import { renderMoviesPage } from "../controllers/moviesController.js"; const router = express.Router(); router.get("/", (req, res) => { - renderMoviesPage(req, res, "POPULAR"); + renderMoviesPage(req, res, "NOW_PLAYING"); }); router.get("/now-playing", (req, res) =>