Skip to content

Commit

Permalink
feat: 카카오 로그인 연결 & 임시 메인 페이지 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
gogumalatte committed Nov 18, 2024
1 parent f256b83 commit 8808d9c
Show file tree
Hide file tree
Showing 16 changed files with 336 additions and 144 deletions.
Binary file removed dist.zip
Binary file not shown.
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/src/assets/tapLogo.svg" />
<link rel="icon" href="tapLogo.svg" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
Expand Down
File renamed without changes
1 change: 0 additions & 1 deletion public/vite.svg

This file was deleted.

Binary file modified src.zip
Binary file not shown.
241 changes: 129 additions & 112 deletions src/Main/MainPage.tsx
Original file line number Diff line number Diff line change
@@ -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<LoginData>({
name: "",
email: "",
});
const [tokens, setTokens] = useState<TokenResponse | null>(null);
const [isLoading, setIsLoading] = useState(false);
const toast = useToast();
import api from "../api/interceptor"; // interceptor.ts에서 설정한 API 인스턴스 가져오기

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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<boolean>(false);
const [randomContents, setRandomContents] = useState<Content[] | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [redirectCountdown, setRedirectCountdown] = useState<number>(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 (
<Box p={4} maxW="400px" mx="auto">
<form onSubmit={handleSubmit}>
<VStack spacing={4}>
<FormControl isRequired>
<FormLabel>이름</FormLabel>
<Input
name="name"
value={formData.name}
onChange={handleChange}
placeholder="이름을 입력하세요"
/>
</FormControl>

<FormControl isRequired>
<FormLabel>이메일</FormLabel>
<Input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
placeholder="이메일을 입력하세요"
/>
</FormControl>
const handleLogout = (): void => {
localStorage.clear();
setIsLoggedIn(false);
navigate("/");
};

<Button
type="submit"
colorScheme="blue"
width="100%"
isLoading={isLoading}
>
로그인
return (
<Center minHeight="100vh" bg="gray.50" padding={4}>
{isLoggedIn ? (
<VStack
spacing={6}
boxShadow="lg"
p={8}
rounded="md"
bg="white"
maxWidth="800px"
textAlign="center"
>
<Heading size="lg" color="teal.500">
환영합니다! 메인 페이지입니다.
</Heading>
<Button colorScheme="teal" onClick={handleLogout}>
로그아웃
</Button>

{tokens && (
<Box
mt={4}
p={4}
borderRadius="md"
bg="gray.50"
width="100%"
shadow="sm"
>
<VStack align="stretch" spacing={3}>
<Box>
<Badge colorScheme="green" mb={1}>
Access Token
</Badge>
<Text fontSize="sm" fontFamily="mono">
{maskToken(tokens.accessToken)}
<Box width="100%">
<Heading size="md" mb={4}>
랜덤 콘텐츠
</Heading>
{isLoading ? (
<Center>
<Spinner size="lg" />
</Center>
) : randomContents ? (
randomContents.map((content) => (
<Box
key={content.id}
p={4}
mb={4}
boxShadow="md"
rounded="lg"
bg="gray.100"
textAlign="left"
>
<Text fontWeight="bold" mb={2}>
{content.title} ({content.releaseYear})
</Text>
</Box>
<Box>
<Badge colorScheme="purple" mb={1}>
Refresh Token
</Badge>
<Text fontSize="sm" fontFamily="mono">
{maskToken(tokens.refreshToken)}
<Text>감독: {content.director || "정보 없음"}</Text>
<Text>출연진: {content.cast || "정보 없음"}</Text>
<Text>국가: {content.country || "정보 없음"}</Text>
<Text>장르: {content.listedIn || "정보 없음"}</Text>
<Text fontSize="sm" mt={2}>
{content.description || "설명이 없습니다."}
</Text>
<Text fontSize="sm" color="gray.500" mt={2}>
추가 날짜: {content.dateAdded}
</Text>
</Box>
</VStack>
</Box>
)}
))
) : (
<Text>표시할 콘텐츠가 없습니다.</Text>
)}
</Box>
</VStack>
</form>
</Box>
) : (
<Box textAlign="center">
<Text fontSize="xl" fontWeight="bold" mb={2}>
로그인하여 서비스를 이용해보세요!
</Text>
<Text fontSize="lg" color="red.500">
{redirectCountdown}초 뒤 로그인 페이지로 이동합니다!
</Text>
</Box>
)}
</Center>
);
};
}

export default LoginForm;
export default MainPage;
59 changes: 59 additions & 0 deletions src/Start/Redirection.tsx
Original file line number Diff line number Diff line change
@@ -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<AuthResponse>(`${BASE_URL}/api/auth/oauth/kakao/callback`, {
params: { code }, // 쿼리 파라미터로 code 전달
})
.then((response: AxiosResponse<AuthResponse>) => {
// 응답에서 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 (
<Box
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="center"
height="100vh"
backgroundColor="gray.100"
>
<Spinner size="xl" color="teal.500" />
<Text fontSize="xl" mt={4}>
카카오 로그인 중입니다...
</Text>
</Box>
);
};

export default Redirection;
42 changes: 42 additions & 0 deletions src/Start/StartPage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Center height="100vh" bg="gray.100">
<VStack spacing={8}>
<Text fontSize={{ base: "2xl", md: "3xl" }} fontWeight="bold">
Welcome to OTT Recommender
</Text>
<Text fontSize={{ base: "md", md: "lg" }} color="gray.600">
Discover the best OTT content tailored for you
</Text>
<Box
as="button"
onClick={handleKakaoLogin}
transition="transform 0.2s"
_hover={{
transform: "scale(1.05)",
}}
_active={{
transform: "scale(0.95)",
}}
>
<Image
src={kakaooButtonImage}
alt="Login with Kakao"
height="48px"
cursor="pointer"
/>
</Box>
</VStack>
</Center>
);
};

export default StartPage;
9 changes: 9 additions & 0 deletions src/Start/assets/kakaoButton.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 8808d9c

Please sign in to comment.