Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions Netflix/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_TMDB_KEY=eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJlNjQzNjUzNTUyY2E5NjIxZGYxMDgwMjk3ODcwMTQ0MiIsIm5iZiI6MTc0MzY4NjE2Ny4zNjYwMDAyLCJzdWIiOiI2N2VlOGExNzU0ZjU5NWJiNTVhN2I3YWYiLCJzY29wZXMiOlsiYXBpX3JlYWQiXSwidmVyc2lvbiI6MX0.KEBQKM1JXCfZKAo7DIf9iLBUKJ3e4GAebLJNsPSaMVY
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.env .gitignore에 추가해서 force push 필요할 듯...

24 changes: 24 additions & 0 deletions Netflix/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
54 changes: 54 additions & 0 deletions Netflix/README.md
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,
},
})
```
28 changes: 28 additions & 0 deletions Netflix/eslint.config.js
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 },
],
},
},
)
13 changes: 13 additions & 0 deletions Netflix/index.html
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>
33 changes: 33 additions & 0 deletions Netflix/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "package",
"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.1.1",
"axios": "^1.8.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.5.0",
"tailwindcss": "^4.1.1"
},
"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"
}
}
1 change: 1 addition & 0 deletions Netflix/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file added Netflix/src/App.css
Empty file.
31 changes: 31 additions & 0 deletions Netflix/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import './App.css';
import HomePage from './pages/HomePage';
import MovieDetailPage from './pages/MovieDetailPage';

import MoviePage from './pages/MoviePage'
import{createBrowserRouter,RouterProvider}from 'react-router-dom'
import NotFoundPage from './pages/NotFoundPage';


const router = createBrowserRouter([
{
path: '/',
element: <HomePage />,
errorElement: <NotFoundPage />,
children: [
{
path: 'movies/:category',
element: <MoviePage />,
},
{
path: 'movie/:movieId',
element: <MovieDetailPage />,
},
],
},
])
function App() {
return <RouterProvider router = {router} />
}

export default App;
1 change: 1 addition & 0 deletions Netflix/src/assets/react.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions Netflix/src/components/LoadingSpinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

export default function LoadingSpinner() {
return (
<div
className='size-12 animate-spin rounded-full border-6 border-t-transparent border-[#b2dab1]'
role = 'status'
>
<span className='sr-only'>loading...</span>
</div>
)
}
37 changes: 37 additions & 0 deletions Netflix/src/components/MovieCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useState } from 'react';
import { Movie } from '../types/movie'
import { useNavigate } from 'react-router-dom';

interface MovieCardProps {
movie: Movie;
}

export default function MovieCard({ movie }: MovieCardProps) {

const [isHovered, setIsHovered] = useState(false);
const navigate = useNavigate()

return (
<div
onClick={() :void | Promise<void> => navigate(`/movie/${movie.id}`)}
className='relative rounded-xl shadow-lg overflow-hidden cursor-pointer w-44 transition-transform duration-300 hover:scale-105'
onMouseEnter = {() : void => setIsHovered(true)}
onMouseLeave = {() : void => setIsHovered(false)}
>
<img
src={`https://image.tmdb.org/t/p/w200${movie.poster_path}`}
alt={`${movie.title} image`} // 이미지가 보이지 않을 경우에 대한 처리
className=''
/>

{isHovered && (
<div className='absolute inset-0 bg-gradient-to-t from-black/50 to-transparent backdrop-blur-md flex flex-col justify-center items-center text-white p-4'>
<h2 className='text-lg font-bold leading-snug'>{movie.title}</h2>
<p className='text-sm text-gray-300 leading-relaxed mt-2 line-clamp-5'>{movie.overview}</p>
</div>
)}
</div>

//line clamp는 5줄 이후 ...
)
}
45 changes: 45 additions & 0 deletions Netflix/src/hooks/useCustomFetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useEffect, useState } from 'react'
import { Movie } from '../types/movie'
import axios from 'axios'

interface ApiResponse<T>{
data:T | null
isPending: boolean
isError: boolean
}

type Language = "ko-KR" | "en-US"


function useCustomFetch<T>(url:string, language:Language='en-US'):ApiResponse<T>{
const [data,setData] = useState<T|null>(null)
const [isPending,setIsPending] = useState(false)
const [isError,setIsError] = useState(false)

useEffect(() => {
const fetchData = async() => {
setIsPending(true)

try {
const {data} = await axios.get<T>(url,{
headers:{
Authorization: `Bearer ${import.meta.env.VITE_TMDB_KEY}`, //환경변수 앞에 접두사 VITE 붙여줘야됨
},
params:{
language,
}
})
setData(data)
} catch {
setIsError(true)
}finally{
setIsPending(false)
}
}
fetchData()
},[url,language])

return{data,isPending,isError}
}

export default useCustomFetch
1 change: 1 addition & 0 deletions Netflix/src/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import 'tailwindcss';
10 changes: 10 additions & 0 deletions Netflix/src/main.tsx
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>,
)
13 changes: 13 additions & 0 deletions Netflix/src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Outlet} from 'react-router-dom'
import { Navbar } from './Navbar'

const HomePage = () => {
return (
<div>
<Navbar />
<Outlet />
</div>
)
}

export default HomePage
71 changes: 71 additions & 0 deletions Netflix/src/pages/MovieDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@

import {Params, useParams} from 'react-router-dom'
import { MovieDetailResponse } from '../types/movie'
import useCustomFetch from '../hooks/useCustomFetch'
import LoadingSpinner from '../components/LoadingSpinner'

const MovieDetailPage =() => {

const params:Readonly<Params<string>> = useParams()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useParams로 가져온 값은 어차피 직접 수정하지 않고, Params도 굳이 쓸 필요 없어 보여서
const { movieId } = useParams<{ movieId: string }>();
이렇게 간단히 타입 정의할 수 있어용

const url = `https://api.themoviedb.org/3/movie/${params.movieId}`
const {isPending, isError, data:movie} = useCustomFetch<MovieDetailResponse>(url,'en-US')

// const [movie,setMovie] = useState<MovieDetailResponse>()
// const [isPending,setIsPending] = useState(false) // loading state
// const [isError,setIsError] = useState(false) // error state


// useEffect(() => {
// const fetchMovies = async () => {
// setIsPending(true)

// try{
// const {data} = await axios.get<MovieDetailResponse>(
// ``,
// {
// headers: {
// Authorization: `Bearer ${import.meta.env.VITE_TMDB_KEY}`,
// }
// },
// );

// setMovie(data);
// }catch{
// setIsError(true);
// }finally{
// setIsPending(false);
// }
// };

// fetchMovies();
// }, [params.movieId]);
Comment on lines +13 to +41
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 왜 주석인가여...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hooks 가 익숙하지 않기 때문에...!


if(isPending){
return (
<div className='flex items-center justify-center h-dvh'>
<LoadingSpinner />
</div>
)
}

if(isError){
return(
<div>
<span className='text-red-500 text-2xl'>에러가 발생했습니다.</span>
</div>
)
}

console.log(params);
return (
<div>
MovieDetailPage{params.movieId}
{movie?.id}
{movie?.production_companies.map((company)=>company.name)}
{movie?.original_title}
{movie?.overview}
</div>
)
}

export default MovieDetailPage
Loading