diff --git a/dist.zip b/dist.zip deleted file mode 100644 index 24f6b88..0000000 Binary files a/dist.zip and /dev/null differ diff --git a/index.html b/index.html index d66aa63..d1d233b 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - + \ No newline at end of file diff --git a/src.zip b/src.zip index 9079e86..fbf138e 100644 Binary files a/src.zip and b/src.zip differ diff --git a/src/Main/MainPage.tsx b/src/Main/MainPage.tsx index a610e6f..ee9bf70 100644 --- a/src/Main/MainPage.tsx +++ b/src/Main/MainPage.tsx @@ -1,137 +1,154 @@ -import React, { useState } from "react"; +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; import { Box, Button, - FormControl, - FormLabel, - Input, + Heading, VStack, + Center, Text, - useToast, - Badge, + Spinner, } from "@chakra-ui/react"; -import { login } from "../api/auth"; -import type { LoginData, TokenResponse } from "../api/type"; -const LoginForm = () => { - const [formData, setFormData] = useState({ - name: "", - email: "", - }); - const [tokens, setTokens] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const toast = useToast(); +import api from "../api/interceptor"; // interceptor.ts에서 설정한 API 인스턴스 가져오기 - const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setFormData((prev) => ({ - ...prev, - [name]: value, - })); - }; +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 maskToken = (token: string) => { - if (token.length <= 8) return "********"; - return token.slice(0, 4) + "..." + token.slice(-4); - }; +function MainPage(): JSX.Element { + const [isLoggedIn, setIsLoggedIn] = useState(false); + const [randomContents, setRandomContents] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [redirectCountdown, setRedirectCountdown] = useState(10); // 5초 카운트다운 + const navigate = useNavigate(); - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsLoading(true); + useEffect(() => { + const accessToken = localStorage.getItem("accessToken"); + const refreshToken = localStorage.getItem("refreshToken"); - try { - const data = await login(formData); - setTokens(data); + if (accessToken && refreshToken) { + setIsLoggedIn(true); + fetchRandomContents(); // 랜덤 콘텐츠를 가져옴 + } else { + setIsLoggedIn(false); - toast({ - title: "로그인 성공", - description: "토큰이 발급되었습니다", - status: "success", - duration: 3000, - isClosable: true, - }); + // 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) { - toast({ - title: "로그인 실패", - description: - error instanceof Error - ? error.message - : "알 수 없는 오류가 발생했습니다", - status: "error", - duration: 3000, - isClosable: true, - }); + console.error("랜덤 콘텐츠를 가져오는 중 오류 발생:", error); } finally { setIsLoading(false); } }; - return ( - -
- - - 이름 - - - - - 이메일 - - + const handleLogout = (): void => { + localStorage.clear(); + setIsLoggedIn(false); + navigate("/"); + }; - - - {tokens && ( - - - - - Access Token - - - {maskToken(tokens.accessToken)} + + + 랜덤 콘텐츠 + + {isLoading ? ( +
+ +
+ ) : randomContents ? ( + randomContents.map((content) => ( + + + {content.title} ({content.releaseYear}) - - - - Refresh Token - - - {maskToken(tokens.refreshToken)} + 감독: {content.director || "정보 없음"} + 출연진: {content.cast || "정보 없음"} + 국가: {content.country || "정보 없음"} + 장르: {content.listedIn || "정보 없음"} + + {content.description || "설명이 없습니다."} + + + 추가 날짜: {content.dateAdded} -
-
- )} + )) + ) : ( + 표시할 콘텐츠가 없습니다. + )} +
-
-
+ ) : ( + + + 로그인하여 서비스를 이용해보세요! + + + {redirectCountdown}초 뒤 로그인 페이지로 이동합니다! + + + )} + ); -}; +} -export default LoginForm; +export default MainPage; diff --git a/src/Start/Redirection.tsx b/src/Start/Redirection.tsx new file mode 100644 index 0000000..2f5ea81 --- /dev/null +++ b/src/Start/Redirection.tsx @@ -0,0 +1,59 @@ +import React, { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import axios, { AxiosResponse } from "axios"; +import { Box, Spinner, Text } from "@chakra-ui/react"; + +// Response 타입 정의 +interface AuthResponse { + accessToken: string; + refreshToken: string; +} + +const Redirection: React.FC = () => { + const navigate = useNavigate(); + const BASE_URL = import.meta.env.VITE_API_BASE_URL; + + useEffect(() => { + // URL에서 code 추출 + const urlParams = new URLSearchParams(window.location.search); + const code: string | null = urlParams.get("code"); + + if (code) { + console.log("Received code:", code); + + // 백엔드에 GET 요청을 보내서 토큰을 가져옴 + axios + .get(`${BASE_URL}/api/auth/oauth/kakao/callback`, { + params: { code }, // 쿼리 파라미터로 code 전달 + }) + .then((response: AxiosResponse) => { + // 응답에서 accessToken과 refreshToken을 로컬스토리지에 저장 + localStorage.setItem("accessToken", response.data.accessToken); + localStorage.setItem("refreshToken", response.data.refreshToken); + // /main으로 이동 + navigate("/main"); + }) + .catch((error: Error) => { + console.error("토큰 요청 에러:", error); + }); + } + }, [navigate, BASE_URL]); + + return ( + + + + 카카오 로그인 중입니다... + + + ); +}; + +export default Redirection; diff --git a/src/Start/StartPage.tsx b/src/Start/StartPage.tsx new file mode 100644 index 0000000..92584ba --- /dev/null +++ b/src/Start/StartPage.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { Center, Text, VStack, Box, Image } from "@chakra-ui/react"; +import kakaooButtonImage from "./assets/kakaoButton.svg"; + +const StartPage: React.FC = () => { + const handleKakaoLogin = () => { + window.location.href = "http://ott.knu-soft.site/api/auth/oauth/kakao"; + }; + + return ( +
+ + + Welcome to OTT Recommender + + + Discover the best OTT content tailored for you + + + Login with Kakao + + +
+ ); +}; + +export default StartPage; diff --git a/src/Start/assets/kakaoButton.svg b/src/Start/assets/kakaoButton.svg new file mode 100644 index 0000000..9a8e36b --- /dev/null +++ b/src/Start/assets/kakaoButton.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/api/auth.ts b/src/api/auth.ts deleted file mode 100644 index 1a300f7..0000000 --- a/src/api/auth.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { API_BASE_URL } from "./constant"; -import { LoginData, TokenResponse } from "./type"; - -export const login = async (data: LoginData): Promise => { - const response = await fetch(`${API_BASE_URL}/api/temp/login`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(data), - }); - - if (!response.ok) { - throw new Error("로그인에 실패했습니다"); - } - - return response.json(); -}; diff --git a/src/api/constant.ts b/src/api/constant.ts deleted file mode 100644 index 01783f1..0000000 --- a/src/api/constant.ts +++ /dev/null @@ -1 +0,0 @@ -export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; diff --git a/src/api/interceptor.ts b/src/api/interceptor.ts new file mode 100644 index 0000000..b59233d --- /dev/null +++ b/src/api/interceptor.ts @@ -0,0 +1,75 @@ +import axios from "axios"; + +// .env 파일의 API 기본 URL 가져오기 +const apiBaseUrl = import.meta.env.VITE_API_BASE_URL; + +// Axios 인스턴스 생성 +const api = axios.create({ + baseURL: apiBaseUrl, // .env에서 지정한 API 기본 URL +}); + +// 리프레시 토큰 요청 함수 +const requestRefreshToken = async () => { + try { + const refreshToken = localStorage.getItem("refreshToken"); // 로컬스토리지에서 리프레시 토큰 가져오기 + if (!refreshToken) { + throw new Error("리프레시 토큰이 없습니다."); + } + + const response = await axios.post(`${apiBaseUrl}/auth/refresh`, { + refreshToken, + }); + + // 새로운 액세스 토큰과 리프레시 토큰 저장 + const { accessToken, refreshToken: newRefreshToken } = response.data; + localStorage.setItem("accessToken", accessToken); + localStorage.setItem("refreshToken", newRefreshToken); + + return accessToken; // 새로운 액세스 토큰 반환 + } catch (error) { + console.error("리프레시 토큰 요청 실패:", error); + localStorage.clear(); + window.location.href = "/login"; // 실패 시 로그아웃 처리 + throw error; + } +}; + +// 요청 인터셉터: Authorization 헤더 추가 +api.interceptors.request.use((config) => { + const accessToken = localStorage.getItem("accessToken"); + if (accessToken) { + config.headers["Authorization"] = `Bearer ${accessToken}`; // 토큰 추가 + } + return config; +}); + +// 응답 인터셉터 +api.interceptors.response.use( + (response) => response, + async (error) => { + const status = error.response?.status; + + if (status === 460) { + // 로그아웃 처리 + localStorage.clear(); + window.location.href = "/login"; + } else if (status === 461) { + // 리프레시 토큰 요청 후 재시도 + try { + const newAccessToken = await requestRefreshToken(); + error.config.headers["Authorization"] = `Bearer ${newAccessToken}`; + return axios(error.config); // 원래 요청 재시도 + } catch (refreshError) { + console.error("리프레시 토큰 갱신 중 오류:", refreshError); + throw refreshError; + } + } else { + // 기타 에러 처리 + console.error("에러가 발생했습니다:", error.message); + } + + return Promise.reject(error); + } +); + +export default api; diff --git a/src/api/type.ts b/src/api/type.ts deleted file mode 100644 index fa8384e..0000000 --- a/src/api/type.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type LoginData = { - name: string; - email: string; -}; - -export type TokenResponse = { - accessToken: string; - refreshToken: string; -}; diff --git a/src/routes/Routes.tsx b/src/routes/Routes.tsx index edc39b3..d107a5e 100644 --- a/src/routes/Routes.tsx +++ b/src/routes/Routes.tsx @@ -1,11 +1,21 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; // 라우터 관련 모듈 가져오기 import MainPage from "../../src/Main/MainPage"; // 메인 페이지 컴포넌트 가져오기 +import StartPage from "../Start/StartPage"; +import RedirectPage from "../Start/Redirection"; import { RouterPath } from "./path"; // 경로 상수 가져오기 // 라우터 정의 const router = createBrowserRouter([ { - path: RouterPath.root, + path: RouterPath.root, // 루트 경로 + element: , // 시작 페이지를 직접 렌더링 + }, + { + path: RouterPath.rediretcion, // 리다이렉션 페이지 경로 + element: , // 리다이렉션 페이지를 직접 렌더링 + }, + { + path: RouterPath.main, // 메인 페이지 경로 element: , // 메인 페이지를 직접 렌더링 }, ]); diff --git a/src/routes/path.ts b/src/routes/path.ts index 9184736..d629d24 100644 --- a/src/routes/path.ts +++ b/src/routes/path.ts @@ -1,4 +1,5 @@ export const RouterPath = { root: "/", - main: "/", // 메인 페이지 + main: "/main", // 메인 페이지 + rediretcion: "/redirection", // 카카오 로그인 리다이렉션 페이지 }; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 11f02fe..8304c33 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1 +1,9 @@ /// + +interface ImportMetaEnv { + readonly VITE_API_BASE_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +}