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 (
-
-
-
-
+ ) : (
+
+
+ 로그인하여 서비스를 이용해보세요!
+
+
+ {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
+
+
+
+
+
+
+ );
+};
+
+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;
+}