-
Notifications
You must be signed in to change notification settings - Fork 0
[week3/mission]useEffect를 활용하여 영화 데이터를 불러오기 #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ba3844f
32e6140
ee060be
497d7c9
aa7b4be
bcc0d8c
b579e19
6887c1f
0379533
7709812
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # Logs | ||
| logs | ||
| *.log | ||
| npm-debug.log* | ||
| yarn-debug.log* | ||
| yarn-error.log* | ||
| pnpm-debug.log* | ||
| lerna-debug.log* | ||
|
|
||
| node_modules | ||
| dist | ||
| dist-ssr | ||
| *.local | ||
| .env | ||
|
|
||
| # Editor directories and files | ||
| .vscode/* | ||
| !.vscode/extensions.json | ||
| .idea | ||
| .DS_Store | ||
| *.suo | ||
| *.ntvs* | ||
| *.njsproj | ||
| *.sln | ||
| *.sw? |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| # React + TypeScript + Vite | ||
|
|
||
| This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. | ||
|
|
||
| Currently, two official plugins are available: | ||
|
|
||
| - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh | ||
| - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh | ||
|
|
||
| ## Expanding the ESLint configuration | ||
|
|
||
| If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: | ||
|
|
||
| ```js | ||
| export default tseslint.config({ | ||
| extends: [ | ||
| // Remove ...tseslint.configs.recommended and replace with this | ||
| ...tseslint.configs.recommendedTypeChecked, | ||
| // Alternatively, use this for stricter rules | ||
| ...tseslint.configs.strictTypeChecked, | ||
| // Optionally, add this for stylistic rules | ||
| ...tseslint.configs.stylisticTypeChecked, | ||
| ], | ||
| languageOptions: { | ||
| // other options... | ||
| parserOptions: { | ||
| project: ['./tsconfig.node.json', './tsconfig.app.json'], | ||
| tsconfigRootDir: import.meta.dirname, | ||
| }, | ||
| }, | ||
| }) | ||
| ``` | ||
|
|
||
| You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: | ||
|
|
||
| ```js | ||
| // eslint.config.js | ||
| import reactX from 'eslint-plugin-react-x' | ||
| import reactDom from 'eslint-plugin-react-dom' | ||
|
|
||
| export default tseslint.config({ | ||
| plugins: { | ||
| // Add the react-x and react-dom plugins | ||
| 'react-x': reactX, | ||
| 'react-dom': reactDom, | ||
| }, | ||
| rules: { | ||
| // other rules... | ||
| // Enable its recommended typescript rules | ||
| ...reactX.configs['recommended-typescript'].rules, | ||
| ...reactDom.configs.recommended.rules, | ||
| }, | ||
| }) | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import js from '@eslint/js' | ||
| import globals from 'globals' | ||
| import reactHooks from 'eslint-plugin-react-hooks' | ||
| import reactRefresh from 'eslint-plugin-react-refresh' | ||
| import tseslint from 'typescript-eslint' | ||
|
|
||
| export default tseslint.config( | ||
| { ignores: ['dist'] }, | ||
| { | ||
| extends: [js.configs.recommended, ...tseslint.configs.recommended], | ||
| files: ['**/*.{ts,tsx}'], | ||
| languageOptions: { | ||
| ecmaVersion: 2020, | ||
| globals: globals.browser, | ||
| }, | ||
| plugins: { | ||
| 'react-hooks': reactHooks, | ||
| 'react-refresh': reactRefresh, | ||
| }, | ||
| rules: { | ||
| ...reactHooks.configs.recommended.rules, | ||
| 'react-refresh/only-export-components': [ | ||
| 'warn', | ||
| { allowConstantExport: true }, | ||
| ], | ||
| }, | ||
| }, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <link rel="icon" type="image/svg+xml" href="/vite.svg" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Vite + React + TS</title> | ||
| </head> | ||
| <body> | ||
| <div id="root"></div> | ||
| <script type="module" src="/src/main.tsx"></script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| { | ||
| "name": "movie", | ||
| "private": true, | ||
| "version": "0.0.0", | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "vite", | ||
| "build": "tsc -b && vite build", | ||
| "lint": "eslint .", | ||
| "preview": "vite preview" | ||
| }, | ||
| "dependencies": { | ||
| "@tailwindcss/vite": "^4.0.17", | ||
| "axios": "^1.8.4", | ||
| "clsx": "^2.1.1", | ||
| "react": "^19.0.0", | ||
| "react-dom": "^19.0.0", | ||
| "react-router-dom": "^7.4.1", | ||
| "tailwindcss": "^4.0.17" | ||
| }, | ||
| "devDependencies": { | ||
| "@eslint/js": "^9.21.0", | ||
| "@types/react": "^19.0.10", | ||
| "@types/react-dom": "^19.0.4", | ||
| "@vitejs/plugin-react-swc": "^3.8.0", | ||
| "eslint": "^9.21.0", | ||
| "eslint-plugin-react-hooks": "^5.1.0", | ||
| "eslint-plugin-react-refresh": "^0.4.19", | ||
| "globals": "^15.15.0", | ||
| "typescript": "~5.7.2", | ||
| "typescript-eslint": "^8.24.1", | ||
| "vite": "^6.2.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { createBrowserRouter, RouterProvider } from "react-router-dom"; | ||
| import { RootLayout } from "./layout/root-layout"; | ||
| import { HomePage } from "./page/HomePage"; | ||
| import { MovieListPage } from "./page/MovieListPage"; | ||
| import { MovieDetailPage } from "./page/MovieDetailPage"; | ||
|
|
||
| const movieListTypes = ["popular", "upcoming", "top_rated", "now_playing"]; | ||
|
|
||
| const movieListRoutes = movieListTypes.map((type) => ({ | ||
| path: `movies/${type}`, | ||
| element: <MovieListPage />, | ||
| })); | ||
|
Comment on lines
+7
to
+12
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 너무 깔끔한 방법이네요! |
||
|
|
||
| const router = createBrowserRouter([ | ||
| { | ||
| path: "/", | ||
| element: <RootLayout />, | ||
| children: [ | ||
| { | ||
| index: true, | ||
| element: <HomePage />, | ||
| }, | ||
| ...movieListRoutes, | ||
| ], | ||
| }, | ||
| { | ||
| path: "movie/detail/:id", | ||
| element: <MovieDetailPage />, | ||
| }, | ||
| ]); | ||
|
|
||
| function App() { | ||
| return <RouterProvider router={router} />; | ||
| } | ||
|
|
||
| export default App; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import axios from "axios"; | ||
|
|
||
| export const apiClient = axios.create({ | ||
| baseURL: "https://api.themoviedb.org/3/movie", | ||
| headers: { | ||
| Authorization: `Bearer ${import.meta.env.VITE_MOVIE_TOKEN}`, | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| export const ErrorPage = () => { | ||
| return ( | ||
| <div className="w-[100vw] h-[100vh] flex items-center justify-center"> | ||
| <div className="text-4xl">영화를 불러올 수 없습니다🥹</div> | ||
| </div> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| export const LoadingPage = () => { | ||
| return ( | ||
| <div className="w-[100vw] h-[100vh] flex items-center justify-center"> | ||
| <span className="w-20 h-20 border-6 border-lime-400 border-b-transparent rounded-full animate-spin"></span> | ||
| </div> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { useNavigate } from "react-router-dom"; | ||
| import { Movie } from "../../types/movieType"; | ||
|
|
||
| interface Props { | ||
| movie: Movie; | ||
| } | ||
|
|
||
| export const MovieCard = ({ movie }: Props) => { | ||
| const navigate = useNavigate(); | ||
| const onClickMovie = (movieId: number) => { | ||
| navigate(`/movie/detail/${movieId}`); | ||
| }; | ||
|
|
||
| return ( | ||
| <div | ||
| className={ | ||
| "w-60 h-90 rounded-md hover:scale-110 transition-all duration-300 ease-in overflow-hidden relative group cursor-pointer" | ||
| } | ||
| onClick={() => onClickMovie(movie.id)} | ||
| > | ||
| <img | ||
| src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`} | ||
| alt={movie.title} | ||
| className="group-hover:blur-[5px]" | ||
| /> | ||
| <div className="opacity-0 absolute top-0 left-0 w-full h-full px-3 flex flex-col items-center justify-center text-white group-hover:opacity-100 z-10"> | ||
| <div className="font-semibold text-center">{movie.title}</div> | ||
| <div className="text-center text-sm"> | ||
| {movie.overview.length > 50 | ||
| ? movie.overview.substring(0, 50) + "..." | ||
| : movie.overview} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import clsx from "clsx"; | ||
|
|
||
| interface Props { | ||
| page: number; | ||
| setPage: React.Dispatch<React.SetStateAction<number>>; | ||
| } | ||
|
|
||
| export const Pagination = ({ page, setPage }: Props) => { | ||
| const onNextPage = (e: React.MouseEvent<HTMLButtonElement>) => { | ||
| e.preventDefault(); | ||
| setPage((prev) => prev + 1); | ||
| }; | ||
|
|
||
| const onPrevPage = (e: React.MouseEvent<HTMLButtonElement>) => { | ||
| e.preventDefault(); | ||
| if (page !== 1) { | ||
| setPage((prev) => prev - 1); | ||
| } | ||
| }; | ||
| return ( | ||
| <div className="flex items-center justify-center w-full h-40 gap-7"> | ||
| <button | ||
| className={clsx( | ||
| "w-15 h-15 rounded-md text-white cursor-pointer", | ||
| page === 1 | ||
| ? "bg-gray-400 hover:bg-gray-400" | ||
| : "bg-lime-500 hover:bg-lime-400" | ||
| )} | ||
| onClick={onPrevPage} | ||
| > | ||
| {"<"} | ||
| </button> | ||
| <div>{page} 페이지</div> | ||
| <button | ||
| className="w-15 h-15 rounded-md bg-lime-500 hover:bg-lime-400 text-white cursor-pointer" | ||
| onClick={onNextPage} | ||
| > | ||
| {">"} | ||
| </button> | ||
| </div> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| @import "tailwindcss"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import clsx from "clsx"; | ||
| import { useNavigate } from "react-router-dom"; | ||
|
|
||
| export const Navbar = () => { | ||
| const navigate = useNavigate(); | ||
| const pathname = window.location.pathname; | ||
|
|
||
| return ( | ||
| <nav className="flex h-15 items-center px-5 gap-4"> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nav도 상수화하고 map으로 순회하며 중복 코드를 줄일 수 있을 것 같아요!
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오 넵넵 따로 컴포넌트화 만들어보겠습니당 |
||
| <div | ||
| onClick={() => navigate("/")} | ||
| className={clsx( | ||
| "cursor-pointer", | ||
| pathname === "/" ? "text-lime-600" : "text-black" | ||
| )} | ||
| > | ||
| 홈 | ||
| </div> | ||
| <div | ||
| onClick={() => navigate("/movies/popular")} | ||
| className={clsx( | ||
| "cursor-pointer", | ||
| pathname === "/movies/popular" ? "text-lime-600" : "text-black" | ||
| )} | ||
| > | ||
| 인기 영화 | ||
| </div> | ||
| <div | ||
| onClick={() => navigate("/movies/upcoming")} | ||
| className={clsx( | ||
| "cursor-pointer", | ||
| pathname === "/movies/upcoming" ? "text-lime-600" : "text-black" | ||
| )} | ||
| > | ||
| 상영 중 | ||
| </div> | ||
| <div | ||
| onClick={() => navigate("/movies/top_rated")} | ||
| className={clsx( | ||
| "cursor-pointer", | ||
| pathname === "/movies/top_rated" ? "text-lime-600" : "text-black" | ||
| )} | ||
| > | ||
| 평점 높은 | ||
| </div> | ||
| <div | ||
| onClick={() => navigate("/movies/now_playing")} | ||
| className={clsx( | ||
| "cursor-pointer", | ||
| pathname === "/movies/now_playing" ? "text-lime-600" : "text-black" | ||
| )} | ||
| > | ||
| 개봉 예정 | ||
| </div> | ||
| </nav> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import { Outlet } from "react-router-dom"; | ||
| import { Navbar } from "./Navbar"; | ||
|
|
||
| export const RootLayout = () => { | ||
| return ( | ||
| <> | ||
| <Navbar /> | ||
| <Outlet /> | ||
| </> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { StrictMode } from 'react' | ||
| import { createRoot } from 'react-dom/client' | ||
| import './index.css' | ||
| import App from './App.tsx' | ||
|
|
||
| createRoot(document.getElementById('root')!).render( | ||
| <StrictMode> | ||
| <App /> | ||
| </StrictMode>, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export const HomePage = () => { | ||
| return <div>HomePage</div>; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
혹시 default export를 안 쓴 이유가 있는지 궁금해용
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아 이거 리팩토링하면서 default가 빠진것같아요...ㅎㅎ