-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f256b83
commit 8808d9c
Showing
16 changed files
with
336 additions
and
144 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.