diff --git a/csr/README.md b/csr/README.md
index 6f2e7c1..84d472c 100644
--- a/csr/README.md
+++ b/csr/README.md
@@ -5,3 +5,26 @@
VITE_TMDB_TOKEN=
```
- npm run dev
+
+
+
+# CSR 프로젝트 이해
+
+- TanStack Router를 사용하여 SPA(Single Page Application) 구조를 구현
+- Jotai를 사용하여 전역 상태를 관리
+- TMDB API를 사용하여 영화 정보를 가져옴
+
+- 컴포넌트 구조
+ - Header: 현재 선택된 영화 목록의 첫 번째 영화를 배너로 표시
+ - Container: 영화 목록을 표시하고, 탭을 통해 다른 카테고리로 전환
+ - Footer: 저작권 정보를 표시
+ - Modal: 영화 상세 정보를 표시
+- 커스텀 훅:
+
+ - useModal: 모달 상태 및 영화 상세 정보 관리
+ - useMovies: 영화 목록 및 선택된 카테고리 관리
+
+- 주요 기능
+ - 영화 목록 표시 (인기순, 상영 중, 평점순, 상영 예정)
+ - 영화 상세 정보 모달
+ - 탭을 통한 카테고리 전환
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/public/scripts/index.js b/ssr/public/scripts/index.js
index 8337712..c897c6b 100644
--- a/ssr/public/scripts/index.js
+++ b/ssr/public/scripts/index.js
@@ -1 +1,13 @@
-//
+document.addEventListener("DOMContentLoaded", () => {
+ const tabItems = document.querySelectorAll(".tab-item");
+
+ tabItems.forEach((item) => {
+ item.addEventListener("mouseover", () => {
+ item.classList.add("hover");
+ });
+
+ item.addEventListener("mouseout", () => {
+ item.classList.remove("hover");
+ });
+ });
+});
diff --git a/ssr/server/apis/movies.js b/ssr/server/apis/movies.js
new file mode 100644
index 0000000..4971aa5
--- /dev/null
+++ b/ssr/server/apis/movies.js
@@ -0,0 +1,17 @@
+import { TMDB_MOVIE_LISTS, TMDB_MOVIE_DETAIL_URL, FETCH_OPTIONS } from "../constants.js";
+
+export const fetchMoviesByCategory = async (category) => {
+ const dd = category === "" ? "NOW_PLAYING" : category;
+ const response = await fetch(TMDB_MOVIE_LISTS[dd], FETCH_OPTIONS);
+ const data = await response.json();
+
+ return data;
+};
+
+export const fetchDetailMovie = async (id) => {
+ const url = TMDB_MOVIE_DETAIL_URL + id;
+ const response = await fetch(url, FETCH_OPTIONS);
+ const data = await response.json();
+
+ return data;
+};
diff --git a/ssr/server/constants.js b/ssr/server/constants.js
new file mode 100644
index 0000000..3082683
--- /dev/null
+++ b/ssr/server/constants.js
@@ -0,0 +1,20 @@
+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/server/routes/index.js b/ssr/server/routes/index.js
index 84d32f2..873a98e 100644
--- a/ssr/server/routes/index.js
+++ b/ssr/server/routes/index.js
@@ -2,19 +2,159 @@ import { Router } from "express";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
+import { fetchMoviesByCategory, fetchDetailMovie } from "../apis/movies.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const router = Router();
-router.get("/", (_, res) => {
+const CATEGORIES = {
+ "now-playing": "상영 중",
+ popular: "인기순",
+ "top-rated": "평점순",
+ upcoming: "상영 예정",
+};
+
+const renderMovieItems = (movies) => {
+ return movies
+ .map(
+ (movie) => `
+
${movie.vote_average.toFixed( + 1 + )}
+ ${movie.title} +들어갈 본문 작성
"; + let template = fs.readFileSync(templatePath, "utf-8"); + + const moviesData = await fetchMoviesByCategory(category); + const movieItems = moviesData.results; + const moviesHTML = renderMovieItems(movieItems); + + const bestMovie = movieItems[0]; + + template = template.replace("", moviesHTML); + + 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.toFixed(1)); + template = template.replace("${bestMovie.title}", bestMovie.title); + template = template.replace("", renderTabItems(category)); + if (id) { + template = template.replace("", await renderMovieItemModal(id)); + } + + return template; +}; + +const renderMovieItemModal = async (id) => { + const movie = await fetchDetailMovie(id); + const genres = movie.genres && movie.genres.map((genre) => genre.name).join(", "); + + return ` ++ ${movie.release_date} · ${genres} +
++ + ${movie.vote_average} +
+${movie.overview}
+