diff --git a/package-lock.json b/package-lock.json index acdcd26..2f530e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@craco/craco": "^7.1.0", "@emotion/react": "^11.13.0", "@emotion/styled": "^11.13.0", + "@react-oauth/google": "^0.12.1", "@tanstack/react-query": "^5.51.15", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", @@ -4722,6 +4723,15 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@react-oauth/google": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.1.tgz", + "integrity": "sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@remix-run/router": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.18.0.tgz", diff --git a/package.json b/package.json index 1340bb1..e56a17d 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@craco/craco": "^7.1.0", "@emotion/react": "^11.13.0", "@emotion/styled": "^11.13.0", + "@react-oauth/google": "^0.12.1", "@tanstack/react-query": "^5.51.15", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", diff --git a/public/images/googleIcon.svg b/public/images/googleIcon.svg new file mode 100644 index 0000000..2b5c694 --- /dev/null +++ b/public/images/googleIcon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/loginLogo.svg b/public/images/loginLogo.svg new file mode 100644 index 0000000..a9c6108 --- /dev/null +++ b/public/images/loginLogo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/App.tsx b/src/App.tsx index e436dd4..5e26f7f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,13 +4,16 @@ import { ChakraProvider } from '@chakra-ui/react'; import { theme } from '@/styles/variants/index'; import { QueryClientProvider } from '@tanstack/react-query'; import { queryClient } from './api'; +import { AuthProvider } from './Provider/Auth'; const App = () => { return ( - + + + diff --git a/src/Provider/Auth.tsx b/src/Provider/Auth.tsx new file mode 100644 index 0000000..633a238 --- /dev/null +++ b/src/Provider/Auth.tsx @@ -0,0 +1,26 @@ +import { AuthContextType, AuthInfo } from '@/types'; +import { createContext, useContext, useState, ReactNode } from 'react'; + +export const AuthContext = createContext(undefined); + +export const AuthProvider = ({ children }: { children: ReactNode }) => { + const [authInfo, setAuthInfo] = useState(undefined); + + const updateAuthInfo = (auth: AuthInfo) => { + if (auth) { + setAuthInfo(auth); + } + }; + + return ( + {children} + ); +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; diff --git a/src/api/hooks/useGetLogin.tsx b/src/api/hooks/useGetLogin.tsx new file mode 100644 index 0000000..749b643 --- /dev/null +++ b/src/api/hooks/useGetLogin.tsx @@ -0,0 +1,22 @@ +import { useQuery } from '@tanstack/react-query'; +import { LoginResponse } from '@/types'; +import { BASE_URL } from '..'; +import axios from 'axios'; + +export type AuthResponse = { + code: string; +}; + +export const getLoginPath = (code: string) => `${BASE_URL}/google/login/redirect?code=${code}`; + +export const getLogin = async ({ code }: AuthResponse): Promise => { + const response = await axios.get(getLoginPath(code)); + return response.data; +}; + +export const useGetLogin = (params: AuthResponse) => { + return useQuery({ + queryKey: ['getLogin', params.code], + queryFn: () => getLogin(params), + }); +}; diff --git a/src/components/HomePage/Contents.tsx b/src/components/HomePage/Contents.tsx index ff2a2a1..9c83eec 100644 --- a/src/components/HomePage/Contents.tsx +++ b/src/components/HomePage/Contents.tsx @@ -1,21 +1,24 @@ import { Grid, GridItem } from '@chakra-ui/react'; import { ContentsInfo } from '@/components/HomePage/ContentsInfo'; import { TestersBox } from './TestersBox'; -import { TopContents } from './TopContents'; export const Contents = () => { return ( - - - - + diff --git a/src/components/HomePage/TestersBox/Header.tsx b/src/components/HomePage/TestersBox/Header.tsx index 25cf29e..0e38290 100644 --- a/src/components/HomePage/TestersBox/Header.tsx +++ b/src/components/HomePage/TestersBox/Header.tsx @@ -1,3 +1,4 @@ +import { breakpoints } from '@/styles/variants'; import styled from '@emotion/styled'; interface HeaderProps { @@ -44,6 +45,12 @@ const Student = styled.div` position: relative; z-index: 2; bottom: -13px; + + @media (max-width: ${breakpoints.md}) { + bottom: 11px; + left: 120px; + box-shadow: ${(props) => (props.isActive === 'univ' ? '4px #6AB9F2' : 'none')}; + } `; const Business = styled.div` @@ -53,6 +60,12 @@ const Business = styled.div` position: relative; z-index: 2; bottom: -13px; + + @media (max-width: ${breakpoints.md}) { + bottom: 11px; + left: 120px; + box-shadow: ${(props) => (props.isActive === 'business' ? '4px #6AB9F2' : 'none')}; + } `; const Bar = styled.div` @@ -61,4 +74,8 @@ const Bar = styled.div` height: 3px; position: absolute; bottom: 8px; + + @media (max-width: ${breakpoints.md}) { + visibility: hidden; + } `; diff --git a/src/components/HomePage/TestersBox/index.tsx b/src/components/HomePage/TestersBox/index.tsx index 3a2beff..f6ea5d1 100644 --- a/src/components/HomePage/TestersBox/index.tsx +++ b/src/components/HomePage/TestersBox/index.tsx @@ -52,6 +52,7 @@ export const TestersBox = () => { h="100%" templateRows={{ base: 'auto 1fr', md: '106px 780px' }} templateColumns={{ base: 'repeat(1, 1fr)', md: 'repeat(1, 1fr)' }} + gap={5} > { - return ( - - - - ); -}; - -const Wrapper = styled.div` - width: 100%; - height: 600px; - display: flex; - align-items: center; - justify-content: center; -`; diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header/HeaderWithout.tsx similarity index 75% rename from src/components/Layout/Header.tsx rename to src/components/Layout/Header/HeaderWithout.tsx index 136fbf7..4eedfd2 100644 --- a/src/components/Layout/Header.tsx +++ b/src/components/Layout/Header/HeaderWithout.tsx @@ -3,14 +3,8 @@ import { Link } from 'react-router-dom'; import { Button } from '@chakra-ui/react'; import { breakpoints } from '@/styles/variants'; import { useMail } from '@/Provider/MailContext'; - -const scrollToSection = (sectionId: string) => { - const element = document.getElementById(sectionId); - if (element) { - const elementPosition = element.getBoundingClientRect().top + window.scrollY - 80; - window.scrollTo({ top: elementPosition, behavior: 'smooth' }); - } -}; +import { RouterPath } from '@/routes/path'; +import { useAuth } from '@/Provider/Auth'; export const Header = () => { const mailContext = useMail(); @@ -32,19 +26,28 @@ export const Header = () => { window.location.reload(); }; + const { authInfo } = useAuth(); + return ( -
Login
- +
+ {authInfo ? ( + + My Page + + ) : ( + + Login + + )} +
+ +
-
- scrollToSection('section2')}> 서비스 체험 - scrollToSection('section3')}> 기능 살펴보기 -
- + AI 메일 생성하기
@@ -120,12 +123,8 @@ const AiButton = styled(Button)` } `; -const MidWrapper = styled.div` +const AuthWrapper = styled.div` cursor: pointer; - margin: 0px 20px; - @media (max-width: ${breakpoints.md}) { - display: none; - } `; const LogoLink = styled(Link)` diff --git a/src/components/Layout/MainHeader.tsx b/src/components/Layout/Header/MainHeader.tsx similarity index 83% rename from src/components/Layout/MainHeader.tsx rename to src/components/Layout/Header/MainHeader.tsx index fe0ec50..5243bc9 100644 --- a/src/components/Layout/MainHeader.tsx +++ b/src/components/Layout/Header/MainHeader.tsx @@ -3,6 +3,8 @@ import { Link } from 'react-router-dom'; import { Button } from '@chakra-ui/react'; import { breakpoints } from '@/styles/variants'; import { useMail } from '@/Provider/MailContext'; +import { RouterPath } from '@/routes/path'; +import { useAuth } from '@/Provider/Auth'; const scrollToSection = (sectionId: string) => { const element = document.getElementById(sectionId); @@ -31,11 +33,24 @@ export const MainHeader = () => { }); }; + const { authInfo } = useAuth(); + return ( -
Login
- +
+ {authInfo ? ( + + My Page + + ) : ( + + Login + + )} +
+ +
@@ -43,7 +58,7 @@ export const MainHeader = () => { scrollToSection('section2')}> 서비스 체험 scrollToSection('section3')}> 기능 살펴보기
- + AI 메일 생성하기 @@ -129,3 +144,7 @@ const LogoLink = styled(Link)` margin-left: 100px; } `; + +const AuthWrapper = styled.div` + cursor: pointer; +`; diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 528b2fd..472220a 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; import { Outlet } from 'react-router-dom'; import { Footer } from './Footer'; -import { HEADER_HEIGHT, MainHeader } from './MainHeader'; +import { HEADER_HEIGHT, MainHeader } from './Header/MainHeader'; import { UpperImage } from './UpperImage'; export const Layout = () => { diff --git a/src/components/Layout/noFooterIndex.tsx b/src/components/Layout/noFooterIndex.tsx index 5803991..a574317 100644 --- a/src/components/Layout/noFooterIndex.tsx +++ b/src/components/Layout/noFooterIndex.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; import { Outlet } from 'react-router-dom'; -import { Header, HEADER_HEIGHT } from './Header'; +import { Header, HEADER_HEIGHT } from './Header/HeaderWithout'; export const NoFooterLayout = () => { return ( diff --git a/src/components/Mail/Header.tsx b/src/components/Mail/Header.tsx index 36482a3..215c691 100644 --- a/src/components/Mail/Header.tsx +++ b/src/components/Mail/Header.tsx @@ -79,6 +79,6 @@ const Bar = styled.div` position: absolute; bottom: 8px; @media (max-width: ${breakpoints.md}) { - width: 400px; + visibility: hidden; } `; diff --git a/src/pages/Login/AuthCallback.tsx b/src/pages/Login/AuthCallback.tsx new file mode 100644 index 0000000..5aa85da --- /dev/null +++ b/src/pages/Login/AuthCallback.tsx @@ -0,0 +1,46 @@ +import React, { useEffect, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { useGetLogin } from '@/api/hooks/useGetLogin'; + +const AuthCallback = () => { + const { search } = useLocation(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [code, setCode] = useState(''); + const navigate = useNavigate(); + + const { data } = useGetLogin({ code }); + + useEffect(() => { + const queryParams = new URLSearchParams(search); + const token = queryParams.get('access_token'); + + if (token) { + // 액세스 토큰을 상태에 저장하고 후속 작업 수행 + setCode(token); + + // 예를 들어, 사용자를 메인 페이지로 리디렉션 + navigate('/home'); + } else { + setError('Failed to retrieve access token'); + } + setLoading(false); + }, [search, navigate]); + + if (loading) { + return

Loading...

; // 로딩 화면 표시 + } + + if (error) { + return

{error}

; // 오류 메시지 표시 + } + + return ( +
+

Authentication Successful

+

Access Token: {code}

{/* 액세스 토큰을 표시하거나 다른 작업 수행 */} +
+ ); +}; + +export default AuthCallback; diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx new file mode 100644 index 0000000..02652ed --- /dev/null +++ b/src/pages/Login/index.tsx @@ -0,0 +1,41 @@ +import styled from '@emotion/styled'; + +export const Login = () => { + return ( + + 로그인 이미지 + + + 구글 아이콘구글 계정으로 로그인 + + + + ); +}; + +const Wrapper = styled.div` + width: 630px; + height: 670px; +`; + +const LoginButton = styled.button` + display: flex; + width: 473px; + height: 50px; + padding: 0px 19px; + justify-content: center; + align-items: center; + gap: 6px; + flex-shrink: 0; + border-radius: 5px; + background: var(--Grey900, #1c1c1e); + color: #fff; + + /* Title5/Inter/Bold/16pt/-2% */ + font-family: Inter; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 140%; /* 22.4px */ + letter-spacing: -0.32px; +`; diff --git a/src/pages/Mail/index.tsx b/src/pages/Mail/index.tsx index 55ff4c6..ba7b05a 100644 --- a/src/pages/Mail/index.tsx +++ b/src/pages/Mail/index.tsx @@ -45,6 +45,7 @@ export const MailPage = () => { h="100%" templateRows={{ base: 'auto auto auto', md: '106px 780px' }} templateColumns={{ base: 'repeat(1, 1fr)', md: 'repeat(1, 1fr)' }} + gap={2} > { onClose={closeModal} /> - { + return
마이 페이지
; +}; diff --git a/src/routes/components/PrivateRoute.tsx b/src/routes/components/PrivateRoute.tsx new file mode 100644 index 0000000..b62aa8f --- /dev/null +++ b/src/routes/components/PrivateRoute.tsx @@ -0,0 +1,13 @@ +import { useAuth } from '@/Provider/Auth'; +import { RouterPath } from '../path'; +import { Navigate, Outlet } from 'react-router-dom'; + +export const PrivateRoute = () => { + const { authInfo } = useAuth(); + + if (authInfo) { + return ; + } + + return ; +}; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index f735b18..ff55c11 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -4,15 +4,25 @@ import { MailPage } from '@/pages/Mail'; import { Layout } from '@/components/Layout'; import { NoFooterLayout } from '@/components/Layout/noFooterIndex'; import { RouterPath } from './path'; -import {Terms} from '@/pages/Extra/Terms'; -import {Privacy} from '@/pages/Extra/Privacy'; -import {Contact} from '@/pages/Extra/Contact'; +import { Terms } from '@/pages/Extra/Terms'; +import { Privacy } from '@/pages/Extra/Privacy'; +import { Contact } from '@/pages/Extra/Contact'; +import { Login } from '@/pages/Login'; +import { MyPage } from '@/pages/MyPage'; +import { PrivateRoute } from './components/PrivateRoute'; +import { Navigate } from 'react-router-dom'; const router = createBrowserRouter([ { path: RouterPath.root, element: , - children: [{ path: RouterPath.home, element: }], + children: [ + { path: RouterPath.home, element: }, + { + path: RouterPath.notFound, + element: , + }, + ], }, { path: RouterPath.root, @@ -22,8 +32,17 @@ const router = createBrowserRouter([ { path: RouterPath.terms, element: }, { path: RouterPath.privacy, element: }, { path: RouterPath.contact, element: }, + { + path: RouterPath.mypage, + element: , + children: [{ path: RouterPath.mypage, element: }], + }, ], }, + { + path: RouterPath.login, + element: , + }, ]); export const Routes = () => { diff --git a/src/routes/path.ts b/src/routes/path.ts index 342c8b7..3561932 100644 --- a/src/routes/path.ts +++ b/src/routes/path.ts @@ -1,8 +1,11 @@ export const RouterPath = { - root: "/", - home: "/", - mail: "/mail", - terms: "/terms", - privacy: "/privacy", - contact: "/contact", -} + root: '/', + home: '/', + mail: '/mail', + terms: '/terms', + privacy: '/privacy', + contact: '/contact', + login: '/login', + mypage: '/mypage', + notFound: '*', +}; diff --git a/src/types/index.ts b/src/types/index.ts index a128e26..8ae841d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -31,3 +31,17 @@ export interface Question { } export type MailInput = mailSendUniv | mailSendBusiness; + +export interface LoginResponse { + accessToken: String; + name: String; + picture: String; + email: String; +} + +export type AuthInfo = LoginResponse; + +export interface AuthContextType { + authInfo?: AuthInfo; + updateAuthInfo: (auth: AuthInfo) => void; +}