Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions Week10/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
VITE_BASE_URL = https://api.themoviedb.org/3
VITE_TMDB_ACCESS_TOKEN = eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2ZmEyZjQ3ZGVhMzdlYTJmYzBkN2Y4NzM5Njk3MjI2OCIsIm5iZiI6MTcyNzkzNjE5My40MDIsInN1YiI6IjY2ZmUzNmMxOWViZWExOTAwNmY3YzZkNSIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.cGR0ZCjkPZiRTIA8jMeboLZqviZoRdpi2Ve221FRgrE
VITE_IMAGE_BASE_URL = https://image.tmdb.org/t/p/w500
24 changes: 24 additions & 0 deletions Week10/.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 Week10/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) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/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 Week10/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 Week10/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>
37 changes: 37 additions & 0 deletions Week10/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "week9",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@headlessui/react": "^2.2.4",
"@reduxjs/toolkit": "^2.8.2",
"@tailwindcss/vite": "^4.1.7",
"axios": "^1.10.0",
"lucide-react": "^0.511.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-redux": "^9.2.0",
"tailwindcss": "^4.1.7",
"zustand": "^5.0.5"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react-swc": "^3.9.0",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.5"
}
}
1 change: 1 addition & 0 deletions Week10/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 Week10/src/App.css
Empty file.
12 changes: 12 additions & 0 deletions Week10/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import "./App.css";
import HomePage from "./pages/HomePage";

function App() {
return (
<>
<HomePage />
</>
);
}

export default App;
8 changes: 8 additions & 0 deletions Week10/src/apis/axiosInstance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import axios from "axios";

export const axiosInstance = axios.create({
headers: {
Authorization: `Bearer ${import.meta.env.VITE_TMDB_ACCESS_TOKEN}`,
},
baseURL: import.meta.env.VITE_BASE_URL,
});
1 change: 1 addition & 0 deletions Week10/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.
26 changes: 26 additions & 0 deletions Week10/src/components/Input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { memo } from "react";

interface InputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
className?: string;
}

const Input = ({
value,
onChange,
placeholder = "검색어를 입력하세요.",
className,
}: InputProps) => {
return (
<input
className={`w-full rounded-md border p-2 border-gray-500 shadow-sm focus:border-blue-500 focus:ring-blue=500 ${className}`}
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
);
};

export default memo(Input);
36 changes: 36 additions & 0 deletions Week10/src/components/LanguageSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { memo } from "react";

interface LanguageOption {
value: string;
label: string;
}

interface LanguageSelectorProps {
value: string;
onChange: (value: string) => void;
options: LanguageOption[];
className?: string;
}

const LanguageSelector = ({
value,
onChange,
options,
className = "",
}: LanguageSelectorProps) => {
return (
<select
value={value}
onChange={(e) => onChange(e.target.value)}
className={`w-full rounded-lg border border-gray-300 px-4 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${className}`}
>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
};

export default memo(LanguageSelector);
44 changes: 44 additions & 0 deletions Week10/src/components/MovieCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { memo } from "react";
import type { Movie } from "../types/movie";

interface MovieCardProps {
movie: Movie;
onClick?: () => void;
}

const MovieCard = ({ movie, onClick }: MovieCardProps) => {
const fallbackImageUrl = "https://via.placeholder.com/640x480";

return (
<div
onClick={onClick}
className="cursor-pointer overflow-hidden rounded-lg bg-white shadow-md transition-all hover:shadow-lg"
>
<div className="relative h-120 overflow-hidden">
<img
src={
movie.poster_path
? `${import.meta.env.VITE_IMAGE_BASE_URL}${movie.poster_path}`
: fallbackImageUrl
}
alt={`${movie.title} poster`}
className="h-full w-full object-cover transition-transform duration-300 ease-in-out hover:scale-105"
/>
<div className="absolute right-2 top-2 rounded-md bg-black px-2 py-1 text-sm font-bold text-white">
{movie.vote_average.toFixed(1)}
</div>
</div>

<div className="p-4">
<h3 className="mb-2 text-lg font-bold text-gray-800">{movie.title}</h3>
<p className="text-sm text-gray-700">
{movie.overview.length > 100
? `${movie.overview.slice(0, 100)}...`
: movie.overview}
</p>
</div>
</div>
);
};

export default memo(MovieCard);
78 changes: 78 additions & 0 deletions Week10/src/components/MovieFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { memo, useCallback, useState } from "react";
import type { MovieFilters } from "../types/movie";
import { MovieLanguages } from "../enums/languages";
import Input from "./Input";

import LanguageSelector from "./LanguageSelector";
import { LANGUAGE_OPTIONS } from "../constants/movie";
import SelectBox from "./SelectBox";

interface MovieFilterProps {
onChange: (filter: MovieFilters) => void;
}

const MovieFilter = ({ onChange }: MovieFilterProps) => {
const [query, setQuery] = useState<string>("");
const [includeAdult, setIncludeAdult] = useState<boolean>(false);
const [language, setLanguage] = useState<MovieLanguages>(
MovieLanguages.KOREAN
);

const handleLanguageChange = useCallback((value: string) => {
setLanguage(value as MovieLanguages);
}, []);

const handleSubmit = () => {
const filters: MovieFilters = {
query,
include_adult: includeAdult,
language,
};

onChange(filters);
};

return (
<div>
<div className="transform space-y-6 rounded-2xl border-gray-300 bg-white p-6 shadow-xl transition-all hover:shadow-2xl">
<div className="flex flex-wrap gap-6">
<div className="min-w-[450px] flex-1">
<label className="mb-2 block text-sm font-medium text-gray-700">
영화 제목
</label>
<Input value={query} onChange={setQuery} />
</div>
<div className="min-w-[250px] flex-1">
<label className="mb-2 block text-sm font-medium text-gray-700">
옵션
</label>
<SelectBox
checked={includeAdult}
onChange={setIncludeAdult}
label="성인 영화 포함"
id="include_adult"
className="w-full rounded-lg border border-gray-300 px-4 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="min-w-[250px] flex-1">
<label className="mb-2 block text-sm font-medium text-gray-700">
언어
</label>
<LanguageSelector
value={language}
onChange={handleLanguageChange}
options={LANGUAGE_OPTIONS}
className="w-full rounded-lg border border-gray-300 px-4 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>

<div className="pt-9">
<button onClick={handleSubmit}>영화 검색</button>
</div>
</div>
</div>
</div>
);
};

export default memo(MovieFilter);
Loading