Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

oAuth, 로그인 유무 로직 구현 #86

Merged
merged 23 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5eb91fc
✨ Feature(#67): useMemo 사용으로 반복 계산 방지 및 렌더링 최적화
bluetree7878 Dec 4, 2024
110c188
Merge branch 'develop' of https://github.com/modern-agile-team/8term-…
bluetree7878 Dec 5, 2024
2af8139
✨ Feature(#67): 로그인/비로그인 확인 useAuth 훅 생성
bluetree7878 Dec 5, 2024
be2c078
✨ Feature(#67): 로그인 시 fake jwt 생성
bluetree7878 Dec 5, 2024
aec6afb
⚙ Setting(#67): CI-CD Action Test - 이미지 삭제
bluetree7878 Dec 5, 2024
7bf4031
📝 Modify(#67): 실제 Service에는 fake jwt 안 올라가게 수정
bluetree7878 Dec 5, 2024
3b05363
📝 Modify(#67): 반환되는 이미지 있는지 확인 후 <none> 태그 이미지가 있을 경우 삭제
bluetree7878 Dec 5, 2024
09e9640
✨ Feature(#67): 토큰 유무 프로필 Modal 변경 및 Interceptor 수정
bluetree7878 Dec 8, 2024
8cb010a
Merge branch 'develop' of https://github.com/modern-agile-team/8term-…
bluetree7878 Dec 8, 2024
360f161
Merge branch 'develop' of https://github.com/modern-agile-team/8term-…
bluetree7878 Dec 9, 2024
c14dc6a
✨ Feature(#useDropdown hook 생성):
bluetree7878 Dec 9, 2024
01aa72a
✨ Feature(#67): useDropdown hook 생성
bluetree7878 Dec 9, 2024
292c387
Merge branch 'feature/#67/OAuth_2_0' of https://github.com/modern-agi…
bluetree7878 Dec 9, 2024
c591ff6
🔨 Refactor(#67): DropdownMenu -> ProfileDropdownMenu로 네이밍 수정
bluetree7878 Dec 9, 2024
4f00d6e
📝 Modify(#67): handleLogin fakeToken 주석
bluetree7878 Dec 9, 2024
3f5865e
📝 Modify(#67): useAuth -> isLoggedIn으로 훅에서 유틸 함수로 변경
bluetree7878 Dec 9, 2024
36d9fb4
🔨 Refactor(#67): interceptor 테스트 콘솔 제거 및 파일명 오탈자 수정
bluetree7878 Dec 9, 2024
5bd9bbd
📝 Modify(#67): usePopover hook 콜백 memoization 및 외부 클릭 감지 기능
bluetree7878 Dec 11, 2024
53d65a8
📝 Modify(#67): usePopover hook 상태 변경 시 호출되는 callback함수 옵셔널 추가 및 Logou…
bluetree7878 Dec 11, 2024
d37014c
🔨 Refactor(#67): isFirstRender -> isFirstMount 네이밍 변경
bluetree7878 Dec 11, 2024
45d71c6
📝 Modify(#67): handleLogin fake Token 주석
bluetree7878 Dec 11, 2024
317b7b5
🔨 Refactor(#67): useIsFirstMount -> useIsMounted hook 사용
bluetree7878 Dec 12, 2024
ad4d371
📝 Modify(#67): handleLogin fake Token 주석
bluetree7878 Dec 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,8 @@ jobs:
# Docker Compose로 서비스 배포
- name: Docker run
run: |
if [ -n "$(docker images -f "dangling=true" -q)" ]; then
docker rmi $(docker images -f "dangling=true" -q)
fi
docker compose -f /home/ubuntu/docker-compose.yml down
docker compose -f /home/ubuntu/docker-compose.yml up -d --pull always
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
href="https://cdn.jsdelivr.net/gh/fonts-archive/Maplestory/subsets/Maplestory-dynamic-subset.css"
type="text/css"
/>
<link rel="icon" href="public/favicon.svg" />
<link rel="icon" href="/favicon.svg" />
<title>CokoEdu</title>
</head>
<body>
Expand Down
18 changes: 11 additions & 7 deletions src/apis/axios/intercepter.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import api from './instance';
import { getCookie } from '@utils/cookies';

// 요청 인터셉터
api.interceptors.request.use(config => {
//요청 성공 직전 호출
//헤더에 인가 토큰 부착
//로컬스토리지에 저장한다고 가정한다면
const accessToken: string | null = localStorage.getItem('Token');
if (accessToken !== null) {
// 쿠키에서 accessToken 가져오기
const accessToken: string | undefined = getCookie('accessToken');
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 알기로는 쿠키의 경우에는 서버에서 받아온 후 굳이 인터셉터에서 추가해주지 않더라도 자동으로 서버에게 전송되는것으로 알고있는데 이러한 로직을 추가하신 이유가 있을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 처음엔 그런 줄 알았으나 User 각각의 토큰이 백엔드에 저장이 따로 안 되기 때문에 요청 시에 유저의 토큰을 보내줘야 백엔드에서 그 토큰을 까서 작업을 해줄 수 있다고 합니다. 예를 들면, 만료시간이나 userImail 등이 있습니다.


return config;
});

// 응답 인터셉터
api.interceptors.response.use(
//http status가 200번대인 경우 호출
response => {
// HTTP 상태 코드가 200번대인 경우
console.log(response);
return response;
},
error => {
// HTTP 상태 코드가 에러인 경우
console.log(error);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

값 체크 후 테스트 console.log는 지워주세요!!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

원래 있었던 코드다 보니 지우면 안 되는 줄 알았습니다! 지우겠습니다~

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아이고 예전에 남아있던 거였나보네요 ㅎㅎ

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#86 (comment)
해당 코멘트에서 이를 수정하였습니다~

//http status가 에러 코드인경우 실행
return Promise.reject(error);
}
);
77 changes: 67 additions & 10 deletions src/common/layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,42 @@
import { useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { getImageUrl } from '@utils/getImageUrl';
import HeaderItem from '../ui/HeaderItem';
import { HeaderBox } from './style';
import * as S from '../ui/style';
import HeaderItem from '../ui/HeaderItem';
import Login from '@features/login/ui/Login';
import { ProfileWrapper, ProfileIcon, HeaderIcon } from '../ui/style';
import useModal from '@hooks/useModal';
import useUserStore from '@/store/useUserStore';
import useAuth from '@hooks/useAuth';
import useDropdown from '@hooks/useDropdown';
import useOutsideClick from '@hooks/useOutsideClick';
import handleLogout from '@features/login/service/handleLogout';

export default function Header() {
const points: number = 2999999999;
const lifePoints: number = 5;

const navigate = useNavigate();
const { isShow, openModal, closeModal, Modal } = useModal();
const { user } = useUserStore();
const { isLoggedIn } = useAuth();

const { isOpen, toggleDropdown, closeDropdown } = useDropdown();
const profileRef = useRef<HTMLDivElement>(null);

// DropdownMenu가 닫히지 않도록 ProfileWrapper를 제외 대상에 추가
const dropdownRef = useOutsideClick(closeDropdown, {
excludeRefs: [profileRef],
});

const handleProfileClick = () => {
if (isLoggedIn) {
toggleDropdown(); // 열림/닫힘 상태 변경
} else {
openModal(); // 로그인 모달 열기
}
};

return (
<HeaderBox>
{user && (
Expand All @@ -27,14 +53,45 @@ export default function Header() {
/>
</>
)}
<ProfileWrapper onClick={openModal}>
<ProfileIcon src={getImageUrl('테두리.svg')} alt="프로필 테두리" />
<HeaderIcon src={getImageUrl('코코-프로필.svg')} alt="코코 프로필" />
</ProfileWrapper>
{/* Modal 컴포넌트 */}
<Modal isShow={isShow}>
<Login openModal={openModal} closeModal={closeModal} />
</Modal>
<S.ProfileWrapper ref={profileRef} onClick={handleProfileClick}>
<S.ProfileIcon src={getImageUrl('테두리.svg')} alt="프로필 테두리" />
<S.HeaderIcon src={getImageUrl('코코-프로필.svg')} alt="코코 프로필" />
{isLoggedIn && isOpen && (
<S.ProfileDropdownMenu
ref={dropdownRef}
onClick={e => e.stopPropagation()}
>
<S.UserNameText>유저이름</S.UserNameText>
<S.UserJoinDate>2024.11.19</S.UserJoinDate>
<S.UserInfoButton
$backgroundColor="#00FAFF;"
$boxShadow="0 2px #00E1EC;"
onClick={() => navigate('/profile')}
>
프로필
</S.UserInfoButton>
<S.UserInfoButton
$backgroundColor="#3DFF4A;"
$boxShadow="0 2px #00EB6A;"
onClick={() => navigate('/setting')}
>
설정
</S.UserInfoButton>
<S.UserInfoButton
$backgroundColor="#FF3F3D;"
$boxShadow="0 2px #EB0000;"
onClick={handleLogout}
>
로그아웃
</S.UserInfoButton>
</S.ProfileDropdownMenu>
)}
</S.ProfileWrapper>
{!isLoggedIn && (
<Modal isShow={isShow}>
<Login openModal={openModal} closeModal={closeModal} />
</Modal>
)}
</HeaderBox>
);
}
2 changes: 2 additions & 0 deletions src/common/layout/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const HeaderBox = styled.header`
height: 42px;
position: fixed;
padding-right: 20px;
z-index: 1;
`;

export const LogoBoxWrapper = styled.div`
Expand All @@ -42,4 +43,5 @@ export const OverRay = styled.div`
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.2);
z-index: 100;
`;
83 changes: 69 additions & 14 deletions src/common/ui/style.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
import styled from 'styled-components';
import styled, { keyframes } from 'styled-components';
import { Link } from 'react-router-dom';

interface SectionButtonProps {
$backgroundImage: string;
interface UserInfoButtonProps {
$backgroundColor: string;
$boxShadow: string;
}

interface MenuButtonProps {
$activeStyle: boolean;
}

interface IconWrapperProps {
$color: string;
}

export const SectionButton = styled.button<SectionButtonProps>`
export const SectionButton = styled.button<{ $backgroundImage: string }>`
width: 100px;
height: 75px;
margin-top: 75px;
Expand All @@ -31,7 +24,7 @@ export const MenuButtonWrapper = styled.nav`
display: inline-block;
`;

export const MenuButton = styled.button<MenuButtonProps>`
export const MenuButton = styled.button<{ $activeStyle: boolean }>`
width: 193px;
height: 42px;
font-size: 15px;
Expand All @@ -58,7 +51,7 @@ export const MenuIcon = styled.img`
height: 26px;
`;

export const IconWrapper = styled.div<IconWrapperProps>`
export const IconWrapper = styled.div<{ $color: string }>`
display: flex;
align-items: center;
margin-right: 16px;
Expand Down Expand Up @@ -90,3 +83,65 @@ export const LogoImg = styled.img`
width: 147px;
height: 117px;
`;

// DropdownMenu 열릴 때 애니메이션
const slideIn = keyframes`
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
`;

export const ProfileDropdownMenu = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: absolute;
margin-top: 3px;
right: 0;
cursor: default;
width: 200px;
height: 200px;
background-color: #fff;
border-radius: 15px;
border: 3px solid #ffb53d;
animation: ${slideIn} 0.3s ease-out;
`;

export const UserNameText = styled.p`
color: #000;
font-size: 18px;
text-align: center;
font-weight: 700;
`;

export const UserJoinDate = styled.p`
font-weight: 300;
color: #cbcbcb;
font-size: 12px;
`;

export const UserInfoButton = styled.button<UserInfoButtonProps>`
width: 80%;
height: 30px;
margin-top: 12px;
border: none;
background-color: ${({ $backgroundColor }) => $backgroundColor};
box-shadow: ${({ $boxShadow }) => $boxShadow};
text-align: center;
font-size: 17px;
font-weight: 700;
color: #ffffff;
border-radius: 6px;
text-shadow: -1px 0 #000, 0 1px #000, 1px 0 #000, 0 -1px #000;
&:hover {
transform: translateY(-2px);
transition: background-color 0.2s ease, transform 0.2s ease, color 0.2s ease,
box-shadow 0.2s ease;
}
`;
23 changes: 23 additions & 0 deletions src/features/login/service/handleLogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// import { setCookie } from '@utils/cookies';

const BASE_URL = import.meta.env.VITE_BASE_URL;

const handleLogin = (provider: 'google' | 'kakao' | 'github') => {
const redirectUrl = `${BASE_URL}/auth/${provider}`;
window.location.href = redirectUrl;

// 테스트용 accessToken 및 refreshToken 생성 및 쿠키에 저장 (실제 Service에 배포할 때는 주석 달거나 삭제)
// const fakeAccessToken = 'test.access.token';
// const fakeRefreshToken = 'test.refresh.token';

// setCookie('accessToken', fakeAccessToken, { path: '/', maxAge: 3600 }); // 1시간 유효
// setCookie('refreshToken', fakeRefreshToken, {
// path: '/',
// maxAge: 3600 * 24 * 30,
// }); // 30일 유효

// alert('AccessToken 생성 완료: ' + fakeAccessToken);
// alert('RefreshToken 생성 완료: ' + fakeRefreshToken);
};

export default handleLogin;
9 changes: 9 additions & 0 deletions src/features/login/service/handleLogout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { removeCookie } from '@utils/cookies';

const handleLogout = () => {
removeCookie('accessToken');
removeCookie('refreshToken');
window.location.reload(); // 로그아웃 후 새로고침
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

새로고침 시, 토큰과, 쿠키가 없는 상태에서, 로그인이 필요한 페이지로 접근시 접근이 가능하거나 의도치 않은 오류가 발생할 수 있지 않나요?

root경로로 이동시키는게 아니라, 새로고침을 한 이유에 대해서 궁금합니다🧐

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용자 편의성을 위해 단순히 로그아웃을 했을 때 그 페이지에 남게끔 하고 싶었습니다. 새로고침을 한 이유는 확실하게 쿠키를 날리고 난 뒤에 로그인/비로그인의 보이는 화면을 달리 하고 싶었던 것이었으나, 생각해보니 말씀하신 것처럼 root 경로로 이동을 안 시켜주면 의도치 않는 오류가 발생할 상황이 생길 수 있겠네요.

import { removeCookie } from '@utils/cookies';

const handleLogout = () => {
  removeCookie('accessToken');
  removeCookie('refreshToken');
  window.location.href = '/';
};

export default handleLogout;

이렇게 코드를 수정하겠습니다. 감사합니다!


export default handleLogout;
8 changes: 0 additions & 8 deletions src/features/login/service/handleSocialLogin.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/features/login/styles.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import styled, { keyframes } from 'styled-components';
import { getImageUrl } from './../../utils/getImageUrl';

interface SocialLoginLinkProps {
$color: string;
Expand Down
8 changes: 4 additions & 4 deletions src/features/login/ui/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
DashLineHr,
} from '../styles';
import { LogoImg } from '@common/ui/style';
import handleSocialLogin from '../service/handleSocialLogin';
import handleLogin from '../service/handleLogin';

interface LoginProps {
openModal: () => void;
Expand All @@ -28,23 +28,23 @@ export default function Login({ closeModal }: LoginProps) {
<SocialLoginButton
$color="#000000"
$backgroundColor="#ffffff"
onClick={() => handleSocialLogin('google')}
onClick={() => handleLogin('google')}
>
<img src={getImageUrl('구글.svg')} alt="구글 로그인" />
Google 로그인
</SocialLoginButton>
<SocialLoginButton
$color="#000000"
$backgroundColor="#FEE500"
onClick={() => handleSocialLogin('kakao')}
onClick={() => handleLogin('kakao')}
>
<img src={getImageUrl('카카오.svg')} alt="카카오 로그인" />
Kakao 로그인
</SocialLoginButton>
<SocialLoginButton
$color="#ffffff"
$backgroundColor="#000000"
onClick={() => handleSocialLogin('github')}
onClick={() => handleLogin('github')}
>
<img src={getImageUrl('깃허브.svg')} alt="깃허브 로그인" />
GitHub 로그인
Expand Down
Loading