Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions 모카_장희정/3주차 미션/mission1/WEB_STUDY
Submodule WEB_STUDY added at 8d92b9
25 changes: 25 additions & 0 deletions 모카_장희정/4주차_미션/.gitignore
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

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env
13 changes: 13 additions & 0 deletions 모카_장희정/4주차_미션/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>Movie App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
35 changes: 35 additions & 0 deletions 모카_장희정/4주차_미션/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"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.3",
"@tanstack/react-query": "^5.74.8",
"@tanstack/react-query-devtools": "^5.74.8",
"axios": "^1.9.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.5.0",
"tailwindcss": "^4.1.3"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.4",
"@vitejs/plugin-react": "^4.3.4",
"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"
}
}
22 changes: 22 additions & 0 deletions 모카_장희정/4주차_미션/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import Home from "./pages/home";
import Movies from "./pages/movies";
import MovieDetail from "./pages/moviedetail";
import Layout from "./components/Navbar";

function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="movies/:category" element={<Movies />} />
<Route path="movie/:id" element={<MovieDetail />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
</Routes>
</BrowserRouter>
);
}

export default App;
31 changes: 31 additions & 0 deletions 모카_장희정/4주차_미션/src/apis/movie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import axios from "axios";

const TMDB_TOKEN = import.meta.env.VITE_TMDB_TOKEN;
const BASE_URL = import.meta.env.VITE_TMDB_BASE_URL;

export type MovieType = "popular" | "now_playing" | "top_rated" | "upcoming";

export const getMovies = async (type: MovieType, page: number = 1) => {
const response = await axios.get(`${BASE_URL}/movie/${type}`, {
headers: {
Authorization: `Bearer ${TMDB_TOKEN}`,
},
params: {
language: "ko-KR",
page,
},
});
return response.data;
};

export const getMovieDetail = async (id: number) => {
const response = await axios.get(`${BASE_URL}/movie/${id}`, {
headers: {
Authorization: `Bearer ${TMDB_TOKEN}`,
},
params: {
language: "ko-KR",
},
});
return response.data;
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions 모카_장희정/4주차_미션/src/components/Error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from "react";

interface ErrorProps {
message: string;
}

const Error = ({ message }: ErrorProps) => {
return (
<div className="error">
<h2>오류가 발생했습니다</h2>
<p>{message}</p>
</div>
);
};

export default Error;
16 changes: 16 additions & 0 deletions 모카_장희정/4주차_미션/src/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ReactElement } from "react";
import { Outlet } from "react-router-dom";
import Navbar from "./Navbar";

const Layout = (): ReactElement => {
return (
<div className="layout">
<Navbar />
<main>
<Outlet />
</main>
</div>
);
};

export default Layout;
13 changes: 13 additions & 0 deletions 모카_장희정/4주차_미션/src/components/Loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ReactElement } from "react";
import React from "react";

const Loading = (): ReactElement => {
return (
<div className="loading-content">
<div className="loading-spinner"></div>
<p className="loading-text">로딩 중...</p>
</div>
);
};

export default Loading;
12 changes: 12 additions & 0 deletions 모카_장희정/4주차_미션/src/components/Loadingspinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ReactElement } from "react";

const LoadingSpinner = (): ReactElement => {
return (
<div className="loading-content">
<div className="loading-spinner"></div>
<p className="loading-text">로딩 중...</p>
</div>
);
};

export default LoadingSpinner;
39 changes: 39 additions & 0 deletions 모카_장희정/4주차_미션/src/components/MovieCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from "react";
import { Link } from "react-router-dom";
import profileImage from "../assets/Profile.png";

interface MovieCardProps {
id: number;
title: string;
poster_path: string;
vote_average: number;
release_date: string;
overview: string;
}

const MovieCard = ({ id, title, poster_path, overview }: MovieCardProps) => {
return (
<Link to={`/movie/${id}`} className="movie-card">
<div className="movie-poster">
<img
src={
poster_path
? `https://image.tmdb.org/t/p/w500${poster_path}`
: profileImage
}
alt={title}
onError={(e) => {
const target = e.target as HTMLImageElement;
target.src = profileImage;
}}
/>
</div>
<div className="movie-overlay">
<h3 className="movie-title">{title}</h3>
<p className="movie-overview">{overview}</p>
</div>
</Link>
);
};

export default MovieCard;
20 changes: 20 additions & 0 deletions 모카_장희정/4주차_미션/src/components/MovieGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// components/MovieGrid.tsx
import React from "react";
import MovieCard from "./MovieCard";
import { Movie } from "../hooks/useCustomFetch";

interface MovieGridProps {
movies: Movie[];
}

const MovieGrid = ({ movies }: MovieGridProps) => {
return (
<div className="movie-grid">
{movies.map((movie) => (
<MovieCard key={movie.id} {...movie} />
))}
</div>
);
};

export default MovieGrid;
41 changes: 41 additions & 0 deletions 모카_장희정/4주차_미션/src/components/MovieList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useCustomFetch, MovieType } from "../hooks/useCustomFetch";
import MovieGrid from "./MovieGrid";
import Loading from "./Loading";
import Error from "./Error";
import Pagination from "./renderPagination";
import React from "react";

interface MovieListProps {
type: MovieType;
}

const MovieList = ({ type }: MovieListProps) => {
const { data, loading, error, goToPage, currentPage } = useCustomFetch({
type,
});

return (
<div className="container">
{loading ? (
<div className="loading-content">
<Loading />
</div>
) : error ? (
<Error message={error.message} />
) : (
<>
{data && data.total_pages > 1 && (
<Pagination
currentPage={currentPage}
totalPages={data.total_pages}
goToPage={goToPage}
/>
)}
<MovieGrid movies={data?.results || []} />
</>
)}
</div>
);
};

export default MovieList;
39 changes: 39 additions & 0 deletions 모카_장희정/4주차_미션/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Link, useLocation, Outlet } from "react-router-dom";
import React from "react";

const Layout = () => {
const location = useLocation();

const links = [
{ path: "/", label: "홈" },
{ path: "/movies/popular", label: "인기" },
{ path: "/movies/now_playing", label: "상영 중" },
{ path: "/movies/top_rated", label: "평점 높은" },
{ path: "/movies/upcoming", label: "개봉 예정" },
];

return (
<div className="layout">
<nav className="navbar">
<div className="navbar-container">
{links.map(({ path, label }) => (
<Link
key={path}
to={path}
className={`nav-link ${
location.pathname === path ? "active" : ""
}`}
>
{label}
</Link>
))}
</div>
</nav>
<main>
<Outlet />
</main>
</div>
);
};

export default Layout;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from "react";

type PaginationProps = {
currentPage: number;
totalPages: number;
goToPage: (page: number) => void;
};

const Pagination = ({ currentPage, totalPages, goToPage }: PaginationProps) => (
<div className="pagination">
<button
className={`pagination-button ${currentPage === 1 ? "disabled" : ""}`}
onClick={() => goToPage(currentPage - 1)}
disabled={currentPage === 1}
>
이전
</button>
<span className="pagination-info">
{currentPage} / {totalPages}
</span>
<button
className={`pagination-button ${
currentPage === totalPages ? "disabled" : ""
}`}
onClick={() => goToPage(currentPage + 1)}
disabled={currentPage === totalPages}
>
다음
</button>
</div>
);

export default Pagination;
Loading