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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.env
48 changes: 48 additions & 0 deletions src/API.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react'
import MovieCard from './components/MovieCard';
//토큰 불러오기
const TMDB_TOKEN = import.meta.env.VITE_TMDB_TOKEN;
const BASE_URL = 'https://api.themoviedb.org/3';

// TMDB 에서 알려준 옵션 설정 / 요청
export const options = {
method: 'GET',
headers: {
accept: 'application/json',
Authorization: `Bearer ${TMDB_TOKEN}` //내 토큰 노출로 AI활용
}
};

// 영화 데이터 가져오는 함수, try catch 구문으로 에러 처리
export const getPopularMovies = async () => {
try {
const response = await fetch(`${BASE_URL}/movie/popular?language=ko-KR&page=1`, options);

if (!response.ok) {
const errorData = await response.json();
console.error("데이터 로딩 에러", errorData);
return [];
}

const data = await response.json();
return data.results;
} catch (err) {
console.error("데이터 로딩 에러", err);
return [];
}
}
//title로 되어있던 props를 MovieId로 변경
export const getMovieDatail = async (MovieId) => {
try {
const response = await fetch(`${BASE_URL}/movie/${MovieId}?language=ko-KR`, options);
const data = await response.json();
return data;
} catch (err) {
console.error("상세 정보 로딩 에러", err);
return null;
}
}




23 changes: 18 additions & 5 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import './App.css'
import movieListData from '../data/movieListData.json'
import MovieCard from './components/MovieCard'
import MovieDatail from './components/MovieDatail'
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import Layout from './components/Layout'
import { getPopularMovies } from './API'
import { useEffect, useState } from 'react'

function Home() {

const [movies, setMovies] = useState([]);
useEffect(() => {
//페이지 렌더링될때 API
const loadMovies = async () => {
const data = await getPopularMovies();
setMovies(data); //상태에 데이터 저장
}
loadMovies();
}, [])

return (
<>
<div className='grid grid-cols-2 md:grid-cols-5 gap-4 p-8 bg-gray-100 min-h-screen content-start'>
{movieListData.results.map((item) => {
{movies.map((item) => {
return (
<>
<MovieCard
Expand All @@ -31,8 +42,10 @@ function App(){
return(
<BrowserRouter>
<Routes>
<Route path = "/" element = { <Home /> }/>
<Route path = "/movie/:title" element = { < MovieDatail /> }/>
<Route path = "/" element = { <Layout /> }>
<Route index element = { <Home /> }/>
<Route path = "movie/:movieId" element = { < MovieDatail /> }/>
</Route>
</Routes>
</BrowserRouter>
)
Expand Down
15 changes: 15 additions & 0 deletions src/components/Layout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react'
import NavBar from './NavBar'
import { Outlet } from 'react-router-dom'


export default function Layout() {
return (
<div>
<NavBar />
<main>
<Outlet />
</main>
</div>
)
}
2 changes: 1 addition & 1 deletion src/components/MovieCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default function MovieCard(props) {
const navigate = useNavigate()

const onClickMovie = () => {
navigate(`/movie/${props.title}`, {state: props})
navigate(`/movie/${props.id}`, {state: props})
}

return (
Expand Down
83 changes: 54 additions & 29 deletions src/components/MovieDatail.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
import React from 'react'
import { useLocation, useParams } from 'react-router-dom'
import React, { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { IMG_URL } from './MovieCard';
import movieDetailData from '../../data/movieDetailData.json';
import { getMovieDatail } from '../API';

export default function MovieDatail() {
const { title } = useParams();
const { state } = useLocation();
export default function MovieDetail() {
const { movieId } = useParams();
const [ movie, setMovie ] = useState(null);
const [ loading, setLoading ] = useState(true);

if (!state) {
return <div className='p-8 min-h-screen'>영화 제목: {title} - 상세 정보를 불러오는 중이거나 데이터가 없습니다.</div>;
useEffect(() => {
const fetchDatail = async () => {
setLoading(true);
const data = await getMovieDatail(movieId);
setMovie(data);
setLoading(false);
}
fetchDatail();
}, [movieId]);

if (loading) {
return <div className='p-8 min-h-screen'>영화 제목: {movieId} - 데이터를 불러오는 중입니다.</div>;
}
if (!movie) {
return <div className='p-8 min-h-screen'>영화 제목: {movieId} - 상세 정보를 불러오는 중이거나 데이터가 없습니다.</div>;
}

return (
// 전체
<div className='flex justify-center p-5 md:p-10 min-h-screen'>
Expand All @@ -21,35 +34,47 @@ export default function MovieDatail() {
<div className='w-[200px] sm:w-[300px] md:w-[400] flex-shrink:0'>
<img
className='w-full rounded-xl shadow-2xl'
src={IMG_URL + state.poster_path}
alt={state.title}/>
src={IMG_URL + movie.poster_path}
alt={movie.title}/>
</div>
{/* 오른쪽 묶음 */}
<div className='flex-1 flex flex-col gap-6'>
{/* 제목과 평점 */}
<div className='border-b pd-4 flex justify-between pd-4 items-end'>
<h1 className='text-2xl sm:text-3xl md:text-5xl font-extrabold'>
{state.title}
{movie.title}
</h1>
<span className='text-lg sm:text-xl font-bold whitespace-nowrap ml-4'>
평점 : {state.vote_average.toFixed(1)}
평점 : {movie.vote_average.toFixed(1)}
</span>
</div>
{/* 장르 정보 */}
<div className='inlime-flex text-center'>
<span className='px-4 py-1.5 rounded-full text-sm font-medium text-center'>
{movieDetailData.genres.map(genre => genre.name).join(', ')}
</span>
</div>
{/* 줄거리 정보 */}
<div className='flex flex-col gap-3'>
<h2 className='text-xl sm:text-2xl font-bold'>
줄거리
</h2>
<p className='leading-relaxed text-base sm:text-lg'>
{state.overview ? state.overview : "내용이 없습니다."}
</p>
</div>
{/* 장르 정보 */}
<div className='flex flex-wrap gap-2'>
{movie.genres.map((genre) => (
<span
key={genre.id}
className='px-4 py-1.5 bg-gray-400 text-white text-sm font-bold rounded-full border border-gray-700 shadow-sm'
>
{genre.name}
</span>
))}
</div>
{/* 기존 장르 정보 */}
{/* <div className='inlime-flex text-center'>
<span className='px-4 py-1.5 rounded-full text-sm font-medium text-center'>
{movie.genres.map(genre => genre.name).join(', ')}
</span>
</div> */}

{/* 줄거리 정보 */}
<div className='flex flex-col gap-3'>
<h2 className='text-xl sm:text-2xl font-bold'>
줄거리
</h2>
<p className='leading-relaxed text-base sm:text-lg'>
{movie.overview ? movie.overview : "내용이 없습니다."}
</p>
</div>
</div>
</div>
</div>
Expand Down
36 changes: 36 additions & 0 deletions src/components/NavBar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react'
import { Link } from 'react-router-dom'

export default function NavBar() {
return (
// siicky CSS로 상단바 고정
<nav className='sticky top-0 z-50 bg-white flex items-center justify-between px-6 py-4 shadow-md border-b'>
{/* 로고영역 */}
<div className='logo flex-shrink:0'>
<Link to="/" className='flex'>
<img src="data:image/png;base64,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"
alt="홈으로"
className='h-8 w-auto object-contain scale-200'
/>
</Link>
</div>

{/* 검색창영역 */}
<div className='search flex-1 max-w-2xl ms-10'>
<div className='relative'>
<input
type="text"
placeholder="영화 제목을 입력하세요"
className='w-full bg-[#3f3f3f] text-white px-5 py-2 rounded-full focus:outline-none focus:ring-2 focus:ring-purple-600 transition-all'
/>
</div>
</div>

{/* 로그인영역 */}
<div className='login flex items-center gap-3 font-semibold text-sm ml-6'>
<button className='border-2 border-purple-600 text-purple-600 hover:bg-purple-600 hover:text-red-500 font-bold py-2 px-4 rounded-md transition-all'>로그인</button>
<button className='border-2 border-purple-600 text-purple-600 hover:bg-purple-600 hover:text-red-500 font-bold py-2 px-4 rounded-md transition-all'>회원가입</button>
</div>
</nav>
)
}