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
17 changes: 17 additions & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@pages/*": ["src/pages/*"],
"@components/*": ["src/components/*"],
"@common/*": ["src/components/common/*"],
"@hooks/*": ["src/hooks/*"],
"@api/*": ["src/api/*"],
"@sb/*": ["src/supabase/*"],
"@sbCtx/*": ["src/supabase/context/*"],
"@utils/*": ["src/utilities/*"]
}
},
"include": ["src"]
}
48 changes: 24 additions & 24 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@supabase/supabase-js": "^2.81.1",
"@supabase/supabase-js": "^2.84.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^7.9.4",
Expand Down
14 changes: 7 additions & 7 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@ import {
SearchResults,
LoginPage,
SignupPage,
} from "./pages";
import Layout from "./components/Layout";

import { useSupabaseAuth } from "./supabase";
import { UserContext } from "./supabase/context/UserContext";

import OAuthCallback from "./components/OAuthCallback";
MyPage,
} from "@pages";
import Layout from "@components/Layout";
import OAuthCallback from "@components/OAuthCallback";
Comment on lines +11 to +12
Copy link
Collaborator

Choose a reason for hiding this comment

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

components에 barrel 만들어서 사용하면 더 좋을 것 같아요


import { useSupabaseAuth } from "@sb";
import { UserContext } from "@sbCtx/UserContext";
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 부분은 context라는 폴더로 만들어서 관리하면 좋습니다! sbCtx라는 명은 너무 불명확 합니다.

export default function App() {
const { getUserInfo } = useSupabaseAuth();
const { setUser } = useContext(UserContext);
Expand All @@ -34,6 +33,7 @@ export default function App() {
<Route index element={<Home />} />
<Route path="details/:id" element={<MovieDetail />} />
<Route path="search" element={<SearchResults />} />
<Route path="mypage" element={<MyPage />} />
</Route>

<Route path="/login" element={<LoginPage />} />
Expand Down
20 changes: 14 additions & 6 deletions src/api/tmdb.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ const options = {
},
};

//인기 영화 가져오기
export async function fetchPopularMovies(page = 1) {
try {
const res = await fetch(
`${BASE_URL}/movie/popular?language=kr-KO&page=${page}`,
`${BASE_URL}/movie/popular?language=ko-KR&page=${page}`,
options
);

if (!res.ok) {
throw new Error("TMDB API 요청 실패");
}
if (!res.ok) throw new Error("TMDB popular API 요청 실패");

const data = await res.json();

//성인영화 제외
const filteredResults = data.results.filter(
(movie) => movie.adult === false
);
Expand All @@ -32,22 +32,28 @@ export async function fetchPopularMovies(page = 1) {
}
}

//영화 상세 정보
export async function fetchMovieDetails(id) {
const res = await fetch(`${BASE_URL}/movie/${id}`, {
const res = await fetch(`${BASE_URL}/movie/${id}?language=ko-KR`, {
headers: {
accept: "application/json",
Authorization: `Bearer ${import.meta.env.VITE_TMDB_ACCESS_TOKEN}`,
},
});

if (!res.ok) throw new Error("TMDB detail API 요청 실패");

return await res.json();
}

//영화 검색
export async function searchMovies(query) {
if (!query) return [];

const res = await fetch(
`${BASE_URL}/search/movie?query=${encodeURIComponent(
query
)}language=ko-KR&page=1`,
)}&language=ko-KR&page=1`,
{
headers: {
accept: "application/json",
Expand All @@ -56,6 +62,8 @@ export async function searchMovies(query) {
}
);

if (!res.ok) throw new Error("TMDB search API 요청 실패");

const data = await res.json();
return data.results || [];
}
2 changes: 1 addition & 1 deletion src/components/Layout.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Outlet } from "react-router-dom";
import NavBar from "./NavBar";
import NavBar from "@components/NavBar";

export default function Layout() {
return (
Expand Down
74 changes: 50 additions & 24 deletions src/components/MovieCard.jsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,56 @@
import { useNavigate } from "react-router-dom";
import { useUser } from "@sbCtx/UserContext";
import { useBookmarks } from "@/context/BookmarkContext";
import { Button } from ".";
Copy link
Collaborator

Choose a reason for hiding this comment

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

절대 경로로 불러오면 더 좋을 것 같아요 !


const baseUrl = "https://image.tmdb.org/t/p/w500";
Copy link
Collaborator

Choose a reason for hiding this comment

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

baseUrl은 constants 폴더에 Urls라는 파일 만들어서 관리하면 더 깔끔해질 것 같아요 !


export default function MovieCard({
title,
poster_path,
vote_average,
size = "md",
}) {
const sizeClass = size === "sm" ? "w-40" : size === "md" ? "w-52" : "w-64";
export default function MovieCard({ id, title, poster_path, vote_average }) {
const navigate = useNavigate();
const { user } = useUser();
const { toggleBookmark, isBookmarked } = useBookmarks();

const handleBookmark = (e) => {
e.stopPropagation();

if (!user) {
alert("로그인이 필요합니다!");
Copy link
Collaborator

Choose a reason for hiding this comment

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

alert는 ux 적으로 좋지 않기 때문에 react toastify와 같은 토스트 라이브러리 사용하는 것이 좋아요!

return;
}

toggleBookmark({ id, title, poster_path, vote_average });
};

return (
<>
<div
className={`${sizeClass} bg-white shadow-md hover:shadow-xl rounded-lg
overflow-hidden cursor-pointer transform hover:scale-105 transition duration-300`}
<div
className="relative cursor-pointer group"
onClick={() => navigate(`/details/${id}`)}
>
{/* 이미지 */}
<img
src={`${baseUrl}${poster_path}`}
alt={title}
className="rounded-lg shadow-md w-full group-hover:opacity-90 transition"
/>

{/* 평점 */}
<span className="absolute bottom-2 left-2 bg-black/70 text-white text-sm px-2 py-1 rounded">
{vote_average}
Copy link
Collaborator

Choose a reason for hiding this comment

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

평점의 경우 toFixed 넣어줘서 반올림 해주면 보기 더 좋습니다 !

</span>

{/* ✅ 북마크 버튼 */}
<Button
onClick={handleBookmark}
className={`absolute top-2 right-2 px-2 py-1 rounded-full text-sm font-bold transition
${
isBookmarked(id)
? "bg-yellow-400 text-black"
: "bg-white/80 text-gray-700"
}
`}
>
<div className="w-full aspect-[2/3]">
<img
src={poster_path ? `${baseUrl}${poster_path}` : "/placeholder.png"}
alt={title}
className="w-full h-72 object-cover"
/>
</div>
<div className="p-4 flex flex-col justify-between h-28">
<h2 className="text-base font-semibold mb-1">{title}</h2>
<p className="text-sm text-gray-500">{vote_average}</p>
</div>
</div>
</>
{isBookmarked(id) ? "★" : "☆"}
</Button>
</div>
);
}
9 changes: 5 additions & 4 deletions src/components/NavBar.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useState, useEffect, useContext } from "react";
import useDebounce from "../hooks/useDebounce";
import useDebounce from "@hooks/useDebounce";
import { Link, useNavigate } from "react-router-dom";
import { UserContext } from "../supabase/context/UserContext";
import { useSupabaseAuth } from "../supabase";
import { Button } from "@/components";
import { UserContext } from "@sbCtx/UserContext";
import { useSupabaseAuth } from "@sb";
import Button from "@common/Button";

export default function NavBar() {
const [search, setSearch] = useState("");
Expand Down Expand Up @@ -75,6 +75,7 @@ export default function NavBar() {
<Link
to="/mypage"
className="block px-4 py-2 hover:bg-gray-100"
onClick={() => setShowMenu(false)}
>
마이페이지
</Link>
Expand Down
6 changes: 3 additions & 3 deletions src/components/OAuthCallback.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useContext } from "react";
import { useSupabaseAuth } from "../supabase";
import { useSupabaseAuth } from "@sb";
import { useNavigate } from "react-router-dom";
import { UserContext } from "@/supabase/context/UserContext";
import { UserContext } from "@sbCtx/UserContext";

export default function OAuthCallback() {
const { handleOAuthCallback } = useSupabaseAuth();
Expand All @@ -21,7 +21,7 @@ export default function OAuthCallback() {

return (
<div className="flex justify-center items-center min-h-screen">
소셜 로그인 처리 중입니다...
로그인 처리 중입니다...
</div>
);
}
1 change: 1 addition & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as Button } from "./common/Button";
export { default as InputField } from "./common/InputField";
export { default as NavBar } from "./NavBar";
export { default as MovieCard } from "./MovieCard";
44 changes: 44 additions & 0 deletions src/context/BookmarkContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { createContext, useContext, useState, useEffect } from "react";

export const BookmarkContext = createContext();

export function BookmarkProvider({ children }) {
const [bookmarks, setBookmarks] = useState([]);

// localStorage에서 복원
useEffect(() => {
const saved = localStorage.getItem("bookmarks");
if (saved) setBookmarks(JSON.parse(saved));
}, []);

// 저장
useEffect(() => {
localStorage.setItem("bookmarks", JSON.stringify(bookmarks));
}, [bookmarks]);

const toggleBookmark = (movie) => {
setBookmarks((prev) => {
const exists = prev.some((m) => m.id === movie.id);
if (exists) {
return prev.filter((m) => m.id !== movie.id);
}
return [...prev, movie];
});
};

const removeBookmark = (id) => {
setBookmarks((prev) => prev.filter((m) => m.id !== id));
};

const isBookmarked = (id) => bookmarks.some((m) => m.id === id);
Comment on lines +6 to +33
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 부분은 비즈니스 로직 분리 해주면 좋을 것 같아요 !


return (
<BookmarkContext.Provider
value={{ bookmarks, toggleBookmark, isBookmarked, removeBookmark }}
>
{children}
</BookmarkContext.Provider>
);
}

export const useBookmarks = () => useContext(BookmarkContext);
Loading