diff --git a/package.json b/package.json index 33e4d90..92f6519 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@chakra-ui/icons": "^2.2.4", "@chakra-ui/react": "2.8.2", "axios": "^1.7.7", "framer-motion": "11.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58d75d0..e5c79e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@chakra-ui/icons': + specifier: ^2.2.4 + version: 2.2.4(@chakra-ui/react@2.8.2(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(framer-motion@11.0.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0) '@chakra-ui/react': specifier: 2.8.2 version: 2.8.2(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(framer-motion@11.0.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -271,6 +274,12 @@ packages: '@chakra-ui/system': '>=2.0.0' react: '>=18' + '@chakra-ui/icons@2.2.4': + resolution: {integrity: sha512-l5QdBgwrAg3Sc2BRqtNkJpfuLw/pWRDwwT58J6c4PqQT6wzXxyNa8Q0PForu1ltB5qEiFb1kxr/F/HO1EwNa6g==} + peerDependencies: + '@chakra-ui/react': '>=2.0.0' + react: '>=18' + '@chakra-ui/image@2.1.0': resolution: {integrity: sha512-bskumBYKLiLMySIWDGcz0+D9Th0jPvmX6xnRMs4o92tT3Od/bW26lahmV2a2Op2ItXeCmRMY+XxJH5Gy1i46VA==} peerDependencies: @@ -2172,6 +2181,11 @@ snapshots: '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(react@18.2.0) react: 18.2.0 + '@chakra-ui/icons@2.2.4(@chakra-ui/react@2.8.2(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(framer-motion@11.0.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)': + dependencies: + '@chakra-ui/react': 2.8.2(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(framer-motion@11.0.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + '@chakra-ui/image@2.1.0(@chakra-ui/system@2.6.2(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.2.0))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.2.0))(@types/react@18.3.12)(react@18.2.0))(react@18.2.0))(react@18.2.0)': dependencies: '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.2.0) diff --git a/src/Main/MainPage.tsx b/src/Main/MainPage.tsx index 54196d0..bfa17e6 100644 --- a/src/Main/MainPage.tsx +++ b/src/Main/MainPage.tsx @@ -1,47 +1,15 @@ import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { - Box, - Button, - Heading, - VStack, - Center, - Text, - Spinner, - keyframes, -} from "@chakra-ui/react"; -import api from "../api/interceptor"; // interceptor.ts에서 설정한 API 인스턴스 가져오기 - -type Content = { - id: number; - showId: string; - type: string; - title: string; - director: string; - cast: string; - country: string; - dateAdded: string; - releaseYear: string; - rating: string; - duration: string; - listedIn: string; - description: string; -}; - -const glitchKeyframes = keyframes` - 0% { transform: translate(0, 0); } - 20% { transform: translate(-5px, 5px); } - 40% { transform: translate(5px, -5px); } - 60% { transform: translate(-3px, 3px); } - 80% { transform: translate(3px, -3px); } - 100% { transform: translate(0, 0); } -`; +import { Box } from "@chakra-ui/react"; +import RecommendedContents from "./components/RecommendedContents"; +import RandomContents from "./components/RandomContents"; +import SearchContents from "./components/SearchContents"; +import NavBar from "./components/NavBar"; +import UnauthorizedAccess from "./components/UnauthorizedAccess"; function MainPage(): JSX.Element { const [isLoggedIn, setIsLoggedIn] = useState(false); - const [randomContents, setRandomContents] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [redirectCountdown, setRedirectCountdown] = useState(10); // 10초 카운트다운 + const [activeTab, setActiveTab] = useState("추천 콘텐츠"); const navigate = useNavigate(); useEffect(() => { @@ -50,123 +18,46 @@ function MainPage(): JSX.Element { if (accessToken && refreshToken) { setIsLoggedIn(true); - fetchRandomContents(); // 랜덤 콘텐츠를 가져옴 } else { setIsLoggedIn(false); - - // 10초 카운트다운 설정 - const interval = setInterval(() => { - setRedirectCountdown((prev) => { - if (prev <= 1) { - clearInterval(interval); // 카운트다운 종료 - navigate("/"); // 로그인 페이지로 이동 - } - return prev - 1; - }); - }, 1000); - - return () => clearInterval(interval); // 컴포넌트 언마운트 시 interval 클리어 } }, [navigate]); - const fetchRandomContents = async () => { - setIsLoading(true); - try { - const response = await api.get("/api/random/3"); // interceptor에서 Authorization 헤더 자동 추가 - const contents = response.data.map( - (item: { content: Content }) => item.content - ); // content만 추출 - setRandomContents(contents); - } catch (error) { - console.error("랜덤 콘텐츠를 가져오는 중 오류 발생:", error); - } finally { - setIsLoading(false); - } - }; - const handleLogout = (): void => { localStorage.clear(); setIsLoggedIn(false); navigate("/"); }; + if (!isLoggedIn) { + return ( + + + + ); + } + return ( -
- {isLoggedIn ? ( - - - 환영합니다! 메인 페이지입니다. - - - - - 랜덤 콘텐츠 - - {isLoading ? ( -
- -
- ) : randomContents ? ( - randomContents.map((content) => ( - - - {content.title} ({content.releaseYear}) - - 감독: {content.director || "정보 없음"} - 출연진: {content.cast || "정보 없음"} - 국가: {content.country || "정보 없음"} - 장르: {content.listedIn || "정보 없음"} - - {content.description || "설명이 없습니다."} - - - 추가 날짜: {content.dateAdded} - - - )) - ) : ( - 표시할 콘텐츠가 없습니다. - )} -
-
- ) : ( - - - 🤬올바른 경로로 접속하지 않았습니다. - - - {redirectCountdown}초 뒤 당신은 사망합니다. - - - )} -
+ + {/* Navigation Bar */} + + + {/* Main Content */} + + {activeTab === "추천 콘텐츠" && } + {activeTab === "랜덤 콘텐츠" && } + {activeTab === "검색" && } + + ); } diff --git a/src/Main/components/NavBar.tsx b/src/Main/components/NavBar.tsx new file mode 100644 index 0000000..f9b3c15 --- /dev/null +++ b/src/Main/components/NavBar.tsx @@ -0,0 +1,103 @@ +import { useEffect, useState } from "react"; +import { + Flex, + Text, + Menu, + MenuButton, + MenuList, + MenuItem, + IconButton, +} from "@chakra-ui/react"; +import { HamburgerIcon } from "@chakra-ui/icons"; +import api from "../../api/interceptor"; + +interface NavBarProps { + activeTab: string; + setActiveTab: (tab: string) => void; + handleLogout: () => void; +} + +const NavBar = ({ + activeTab, + setActiveTab, + handleLogout, +}: NavBarProps): JSX.Element => { + const [userName, setUserName] = useState(""); + + useEffect(() => { + const fetchUserName = async () => { + try { + const response = await api.get("/api/members/info"); + const name = response.data.name; + setUserName(name); + } catch (error) { + console.error("사용자 정보를 가져오는 중 오류 발생:", error); + } + }; + + fetchUserName(); + }, []); + + return ( + + + setActiveTab("추천 콘텐츠")} + > + 추천 콘텐츠 + + setActiveTab("랜덤 콘텐츠")} + > + 랜덤 콘텐츠 + + setActiveTab("검색")} + > + 검색 + + + + } + variant="outline" + /> + + {/* 사용자 이름 표시 */} + {userName ? `${userName} 님` : "사용자 님"} + 로그아웃 + + + + ); +}; + +export default NavBar; diff --git a/src/Main/components/RandomContents.tsx b/src/Main/components/RandomContents.tsx new file mode 100644 index 0000000..88a5bc7 --- /dev/null +++ b/src/Main/components/RandomContents.tsx @@ -0,0 +1,5 @@ +function RandomContents(): JSX.Element { + return
랜덤 콘텐츠 컴포넌트 내용
; +} + +export default RandomContents; diff --git a/src/Main/components/RecommendedContents.tsx b/src/Main/components/RecommendedContents.tsx new file mode 100644 index 0000000..10fa14e --- /dev/null +++ b/src/Main/components/RecommendedContents.tsx @@ -0,0 +1,5 @@ +function RecommendedContents(): JSX.Element { + return
추천 콘텐츠 컴포넌트 내용
; +} + +export default RecommendedContents; diff --git a/src/Main/components/SearchContents.tsx b/src/Main/components/SearchContents.tsx new file mode 100644 index 0000000..0036301 --- /dev/null +++ b/src/Main/components/SearchContents.tsx @@ -0,0 +1,5 @@ +function SearchContents(): JSX.Element { + return
검색 콘텐츠 컴포넌트 내용
; +} + +export default SearchContents; diff --git a/src/Main/components/UnauthorizedAccess.tsx b/src/Main/components/UnauthorizedAccess.tsx new file mode 100644 index 0000000..d1d2b67 --- /dev/null +++ b/src/Main/components/UnauthorizedAccess.tsx @@ -0,0 +1,48 @@ +import React, { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Box, Text, keyframes } from "@chakra-ui/react"; + +const glitchKeyframes = keyframes` + 0% { transform: translate(0, 0); } + 20% { transform: translate(-5px, 5px); } + 40% { transform: translate(15px, -5px); } + 60% { transform: translate(-3px, 30px); } + 80% { transform: translate(3px, -3px); } + 100% { transform: translate(0, 0); } +`; + +const UnauthorizedAccess: React.FC = () => { + const [redirectCountdown, setRedirectCountdown] = useState(10); + const navigate = useNavigate(); + + useEffect(() => { + const interval = setInterval(() => { + setRedirectCountdown((prev) => { + if (prev <= 1) { + clearInterval(interval); + navigate("/"); + } + return prev - 1; + }); + }, 1000); + + return () => clearInterval(interval); + }, [navigate]); + + return ( + + + 🤬올바른 경로로 접속하지 않았습니다. + + + {redirectCountdown}초 뒤 당신은 사망합니다. + + + ); +}; + +export default UnauthorizedAccess; diff --git a/src/Start/StartPage.tsx b/src/Start/StartPage.tsx index c3c879d..9e9a742 100644 --- a/src/Start/StartPage.tsx +++ b/src/Start/StartPage.tsx @@ -14,7 +14,7 @@ const StartPage: React.FC = () => { Welcome to OTT Recommender - + Discover the best OTT content tailored for you @@ -45,7 +45,6 @@ const StartPage: React.FC = () => { fontSize="sm" fontWeight="light" textAlign={"center"} - mt={50} > Software Design Course in Kyungpook National University Copyright(c) 2024, Lee Jiho, Choi Kiyeong