-
Notifications
You must be signed in to change notification settings - Fork 0
Create Week9 Mission1,2,3, #113
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
base: main
Are you sure you want to change the base?
Changes from 1 commit
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,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? |
| 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, | ||
| }, | ||
| }) | ||
| ``` |
| 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,32 @@ | ||
| { | ||
| "name": "mission1", | ||
| "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.8", | ||
| "axios": "^1.9.0", | ||
| "react": "^19.1.0", | ||
| "react-dom": "^19.1.0", | ||
| "tailwindcss": "^4.1.8" | ||
| }, | ||
| "devDependencies": { | ||
| "@eslint/js": "^9.25.0", | ||
| "@types/react": "^19.1.2", | ||
| "@types/react-dom": "^19.1.2", | ||
| "@vitejs/plugin-react": "^4.4.1", | ||
| "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" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| #root { | ||
| max-width: 1280px; | ||
| margin: 0 auto; | ||
| padding: 2rem; | ||
| text-align: center; | ||
| } | ||
|
|
||
| .logo { | ||
| height: 6em; | ||
| padding: 1.5em; | ||
| will-change: filter; | ||
| transition: filter 300ms; | ||
| } | ||
| .logo:hover { | ||
| filter: drop-shadow(0 0 2em #646cffaa); | ||
| } | ||
| .logo.react:hover { | ||
| filter: drop-shadow(0 0 2em #61dafbaa); | ||
| } | ||
|
|
||
| @keyframes logo-spin { | ||
| from { | ||
| transform: rotate(0deg); | ||
| } | ||
| to { | ||
| transform: rotate(360deg); | ||
| } | ||
| } | ||
|
|
||
| @media (prefers-reduced-motion: no-preference) { | ||
| a:nth-of-type(2) .logo { | ||
| animation: logo-spin infinite 20s linear; | ||
| } | ||
| } | ||
|
|
||
| .card { | ||
| padding: 2em; | ||
| } | ||
|
|
||
| .read-the-docs { | ||
| color: #888; | ||
| } |
|
Member
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. ๐Challenge ๋ฏธ์ ์ ๋ฐ๋ผ ํ๋ก์ ํธ ์ ์ฒด์ ์ฑ๋ฅ ์ต์ ํ๋ UX ๊ฐ์ ์๋ ๋์ ํด๋ณด์ธ์! ์ฝ๋ ํ์ง ์ธก๋ฉด์์๋ ์ค๋ณต ์ ๊ฑฐ, ์ฑ ์ ๋ถ๋ฆฌ, ์์ธ ์ฒ๋ฆฌ ๊ฐ์ ๋ฑ์ ํตํด ๊ฐ๋ ์ฑ๊ณผ ์ ์ง๋ณด์์ฑ์ด ์ข์ ์ฝ๋๋ก ๋ฆฌํฉํ ๋ง ํด๋ณด๋ฉด ์ด๋จ๊น์ ๐ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import Homepage from "./pages/Homepage"; | ||
|
|
||
| function App() { | ||
| return <Homepage />; | ||
| } | ||
| export default App; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| interface InputProps { | ||
| value: string; | ||
| onChange: (value: string) => void; | ||
| placeholder?: string; | ||
| className?: string; | ||
| } | ||
|
|
||
| export const Input = ({ | ||
| value, | ||
| onChange, | ||
| placeholder = "๊ฒ์์ด๋ฅผ ์ ๋ ฅํ์ธ์ฌ.", | ||
| className = "", | ||
| }: InputProps) => { | ||
| return ( | ||
| <input | ||
| value={value} | ||
| onChange={(e) => onChange(e.target.value)} | ||
| placeholder={placeholder} | ||
| className={`w-full rounded-md border border-gray-600 p-2 shadow-md focus:border-blue-500 focus:ring-blue-500 ${className}`} | ||
| /> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| interface LaguageOption { | ||
| value: string; | ||
| label: string; | ||
| } | ||
| interface LanguageSelectorProps { | ||
| value: string; | ||
| onChange: (value: string) => void; | ||
| options: LaguageOption[]; | ||
| className?: string; | ||
| } | ||
| export 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> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import type { Movie } from "../types/movie"; | ||
|
|
||
| interface MovieCardProps { | ||
| movie: Movie; | ||
| onClick: () => void; | ||
| } | ||
| const MovieCard = ({ movie, onClick }: MovieCardProps) => { | ||
| const imageBaseUrl = "https://image.tmdb.org/t/p/w500"; | ||
| const fallbackImageImage = "..."; | ||
| return ( | ||
| <div | ||
| onClick={onClick} | ||
| className="overflow-hidden rounded-lg bg-white shadow-md transition-all hover:shadow-lg" | ||
| > | ||
| <div className="relative h-80 overflow-hidden"> | ||
| <img | ||
| src={ | ||
| movie.poster_path | ||
| ? `${imageBaseUrl}${movie.poster_path}` | ||
| : fallbackImageImage | ||
| } | ||
| alt={`${movie.title}ํฌ์คํฐ`} | ||
| className="h-full w-full ease-in-out object-cover transition-transform duration-300 ease-in-out hover:scale-110" | ||
| /> | ||
| <div className="absolute right-2 top-2 rounded-md bg-blue-500 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-600"> | ||
| {movie.release_date} | {movie.original_language.toUpperCase()} | ||
| </p> | ||
| <p className="mt-2 text-sm text-gray-700"> | ||
| {movie.overview.length > 100 | ||
| ? `${movie.overview.slice(0, 100)}...` | ||
| : movie.overview} | ||
| </p> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default MovieCard; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import type { Movie } from "../types/movie"; | ||
|
|
||
| interface MovieModalProps { | ||
| movie: Movie; | ||
| onClose: () => void; | ||
| } | ||
|
|
||
| const MovieModal = ({ movie, onClose }: MovieModalProps) => { | ||
| const imageBaseUrl = "https://image.tmdb.org/t/p/w500"; | ||
|
|
||
| return ( | ||
| <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-70"> | ||
| <div className="relative max-w-3xl w-full bg-white rounded-xl shadow-lg overflow-hidden"> | ||
| <button | ||
| className="absolute top-4 right-4 text-white text-xl z-10" | ||
| onClick={onClose} | ||
| > | ||
| โ | ||
| </button> | ||
| <div | ||
| className="h-60 bg-cover bg-center" | ||
| style={{ | ||
| backgroundImage: `url(https://image.tmdb.org/t/p/original${movie.backdrop_path})`, | ||
| }} | ||
| /> | ||
| <div className="grid grid-cols-1 md:grid-cols-3 p-6 gap-6"> | ||
| <img | ||
| src={`${imageBaseUrl}${movie.poster_path}`} | ||
| alt={movie.title} | ||
| className="w-full h-auto rounded-lg shadow-md md:col-span-1" | ||
| /> | ||
|
|
||
| <div className="md:col-span-2 space-y-3"> | ||
| <h2 className="text-2xl font-bold">{movie.title}</h2> | ||
| <p className="text-gray-500 italic">{movie.original_title}</p> | ||
|
|
||
| <div className="flex items-center gap-2"> | ||
| <span className="text-blue-600 font-bold text-lg"> | ||
| {movie.vote_average.toFixed(1)} | ||
| </span> | ||
| <span className="text-sm text-gray-500"> | ||
| ({movie.vote_count.toLocaleString()} ํ๊ฐ) | ||
| </span> | ||
| </div> | ||
|
|
||
| <div className="text-sm text-gray-700"> | ||
| <p>๊ฐ๋ด์ผ: {movie.release_date}</p> | ||
| <p>์ธ๊ธฐ: {movie.popularity}</p> | ||
| </div> | ||
|
|
||
| <div className="text-sm mt-2 text-gray-800 leading-relaxed"> | ||
| {movie.overview || "์ค๊ฑฐ๋ฆฌ ์ ๋ณด๊ฐ ์์ต๋๋ค."} | ||
| </div> | ||
|
|
||
| <div className="flex gap-2 mt-4"> | ||
| <a | ||
| href={`https://www.imdb.com/find?q=${encodeURIComponent( | ||
| movie.title | ||
| )}`} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm" | ||
| > | ||
| IMDb์์ ๊ฒ์ | ||
| </a> | ||
| <button | ||
| onClick={onClose} | ||
| className="px-4 py-2 border border-gray-300 rounded text-sm hover:bg-gray-100" | ||
| > | ||
| ๋ซ๊ธฐ | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default MovieModal; |
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.
ํ์ฌ ์ฝ๋๋ฅผ ๋ณด๋ฉด 10์ฃผ์ฐจ ์ํฌ๋ถ ๋ฏธ์ ์ธ ๊ฒ์ ๊ธฐ๋ฅ์ ์ง์คํ ๊ตฌ์กฐ๋ก ๋์ด ์๋๋ฐ, ์ง๋ ๋ฏธ์ ์ด์๋ ์ํ ํ์ด์ง์ ํตํฉํ๋ฉด ๋ ์์ฑ๋ ๋์ ์ํ ์ฌ์ดํธ๊ฐ ๋ ๊ฒ ๊ฐ์ต๋๋ค!