Skip to content

Commit

Permalink
소셜 로그인(회원가입) 기능 구현 (#120)
Browse files Browse the repository at this point in the history
* feat: (#74) 로그인, 리다이렉션 페이지 구현

* feat: (#74) 로그인 정보에 대한 context 구현

* refactor: (#74) 실제 API 연동을 위한 url path로 대체

* design: (#74) 선택지 리스트 모바일의 경우 스크롤 없도록 수정

* chore: (#74) 카카오 로그인 버튼 svg 파일 추가

* feat: (#74) 페이지 라우팅 구현

* chore: (#74) request의 key 값 수정

* 회원 닉네임 수정, 회원 탈퇴 fetch 함수 구현 및 MSW 코드 작성 (#178)

* refactor: (#153) delete를 패치하는 함수 오류 제거를 위한 리팩터링

* feat: (#153): 유저 닉네임 변경, 회원 탈퇴 MSW 코드 작성

* feat: (#153) 유저 닉네임, 회원 탈퇴 api fetch 함수 구현

* refactor: (#153) BASE_URL 추가 및 MSW 코드 성공했을 때 구체적인 메세지로 수정

* fix: (#74) 로그인 후 context API에 저장이 안되는 오류 해결

* feat: (#74) 쿠키 getter, setter 함수 제작

* feat: (#74) 로그인 후 쿠키에 토큰을 저장하고 context API에 토큰 저장

* refactor: (#74) 로그인 정보 변수 타입 위치 이동

* feat: (#74) 레이아웃 컴포넌트에 전역 로그인 정보 적용하기

* feat: (#74) 초기 진입 시 쿠키 내 엑세스 토큰 확인/설정하는 코드 작성

* fix: (#74) 로그인 관련 라우팅이 안되는 오류 해결

* feat: (#74) 유저 로그인 쿼리에 현 로그인 여부를 키로 추가

* feat: (#74) 글쓰기 api url를 실제 url로 수정

* refactor: (#74) 유저정보 훅 이름 변경에 따른 수정

* feat: (#74) 글목록 페이지에 전역 유저정보 적용

* fix: 중복 코드로 인한 오류 수정

* fix: 스타일드 컴포넌트 프롭스 오타 오류 수정

* chore: 허스키 파일 업데이트 없음

* refactor: 불필요한 코드 삭제

* refactor: 엑세스 토큰 타입 파일 분리

* refactor: (#74) 로그인 정보 관련 이름 수정, login > logged

* feat: (#74) fetch 유틸함수에서 쿠키를 불러와 토큰 넣은 헤더 생성

* fix: (#74) 쿠키가 브라우저에 저장 안되는 오류 수정

- path=/ 를 통해 모든 url에서 쿠키 접근가능하도록 수정

* feat: (#74) 패치 헤더에 직접 토큰을 넣는 방식으로 수정함에 따른 기존 코드 수정

* feat: (#74)사용자 정보 가지고 오는 api 실제 dev서버 url로 수정

* fix: (#74) 로그인 정보 - 사용자 정보 불러오기 무한루프 오류 해결

* refactor: (#74) api 연결 url 이름 규칙에 맞게 수정

* refactor: (#74) 쿼리스트링 가지고 오는 방식 수정

* feat: (#74)  .env url 규칙에 따라 수정

---------

Co-authored-by: 김영길/KIM YOUNG GIL <80146176+Gilpop8663@users.noreply.github.com>
Co-authored-by: chsua <csj1919@naver.com>
  • Loading branch information
3 people authored Aug 2, 2023
1 parent df0007c commit 28e2e7c
Show file tree
Hide file tree
Showing 19 changed files with 264 additions and 43 deletions.
Empty file modified frontend/.husky/pre-commit
100644 → 100755
Empty file.
Empty file modified frontend/.husky/pre-push
100644 → 100755
Empty file.
16 changes: 10 additions & 6 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { RouterProvider } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from 'styled-components';

import { AuthProvider } from '@hooks/context/auth';

import router from '@routes/router';

import { GlobalStyle } from '@styles/globalStyle';
Expand All @@ -11,12 +13,14 @@ import { theme } from '@styles/theme';
const queryClient = new QueryClient();

const App = () => (
<ThemeProvider theme={theme}>
<GlobalStyle />
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</ThemeProvider>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<ThemeProvider theme={theme}>
<GlobalStyle />
<RouterProvider router={router} />
</ThemeProvider>
</AuthProvider>
</QueryClientProvider>
);

export default App;
6 changes: 4 additions & 2 deletions frontend/src/api/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
deleteFetch,
} from '@utils/fetch';

const BASE_URL = process.env.VOTOGETHER_BASE_URL;

export const votePost = async (postId: number, optionId: number) => {
return await postFetch(`/posts/${postId}/options/${optionId}`, '');
};
Expand All @@ -29,11 +31,11 @@ export const getPost = async (postId: number): Promise<PostInfo> => {
};

export const createPost = async (newPost: FormData) => {
return await multiPostFetch('/posts', newPost);
return await multiPostFetch(`${BASE_URL}/posts`, newPost);
};

export const editPost = async (postId: number, updatedPost: FormData) => {
return await multiPutFetch(`/posts/${postId}`, updatedPost);
return await multiPutFetch(`http://3.35.232.54/api/posts/${postId}`, updatedPost);
};

export const removePost = async (postId: number) => {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/api/wus/userInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const transformUserInfoResponse = (userInfo: UserInfoResponse): User => {
};
};

const BASE_URL = process.env.VOTOGETHER_MOCKING_URL;
const BASE_URL = process.env.VOTOGETHER_BASE_URL;

export const getUserInfo = async () => {
const userInfo = await getFetch<UserInfoResponse>(`${BASE_URL}/members/me`);
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/assets/kakao_login_medium_wide.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion frontend/src/components/PostForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export default function PostForm({ data, mutate, isError, error }: PostFormProps
// 글 수정의 경우 작성시간을 기준으로 마감시간 옵션을 더한다.
// 마감시간 옵션을 선택 안했다면 기존의 마감 시간을 유지한다.
};
formData.append('texts', JSON.stringify(updatedPostTexts));
formData.append('request', JSON.stringify(updatedPostTexts));

mutate(formData);

Expand Down
10 changes: 6 additions & 4 deletions frontend/src/components/common/Layout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { PropsWithChildren } from 'react';
import { PropsWithChildren, useContext } from 'react';
import { useNavigate } from 'react-router-dom';

import { AuthContext } from '@hooks/context/auth';

import Dashboard from '@components/common/Dashboard';
import WideHeader from '@components/common/WideHeader';

Expand All @@ -15,8 +17,8 @@ interface LayoutProps extends PropsWithChildren {
export default function Layout({ children, isSidebarVisible }: LayoutProps) {
const navigate = useNavigate();

//추후 구현 예정
const userInfo = undefined;
const { loggedInfo } = useContext(AuthContext);

const categoryList = MOCK_FAVORITE_CATEGORIES;
const selectedCategory = undefined;
const handleLogoutClick = () => {};
Expand All @@ -34,7 +36,7 @@ export default function Layout({ children, isSidebarVisible }: LayoutProps) {
{isSidebarVisible && (
<S.DashboardWrapper>
<Dashboard
userInfo={userInfo}
userInfo={loggedInfo.userInfo}
categoryList={categoryList}
selectedCategory={selectedCategory}
handleLogoutClick={handleLogoutClick}
Expand Down
10 changes: 4 additions & 6 deletions frontend/src/components/post/PostListPage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Suspense } from 'react';
import { Suspense, useContext } from 'react';

import { AuthContext } from '@hooks/context/auth';
import { useCategoryList } from '@hooks/query/category/useCategoryList';
import { useUserInfo } from '@hooks/query/user/useUserInfo';
import { useDrawer } from '@hooks/useDrawer';

import AddButton from '@components/common/AddButton';
Expand All @@ -21,10 +21,8 @@ import * as S from './style';
export default function PostListPage() {
const { drawerRef, closeDrawer, openDrawer } = useDrawer('left');

//추후 구현 예정
const isLoggedIn = true; //로그인한 유저라고 가정
const { data: categoryList } = useCategoryList(isLoggedIn);
const { data: userInfo } = useUserInfo();
const { isLogged, userInfo } = useContext(AuthContext).loggedInfo;
const { data: categoryList } = useCategoryList(isLogged);

const handleLogoutClick = () => {};

Expand Down
37 changes: 37 additions & 0 deletions frontend/src/hooks/context/auth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, { Dispatch, SetStateAction, createContext, useEffect, useState } from 'react';

import { LoggedInfo } from '@type/user';

import { useUserInfo } from '@hooks/query/user/useUserInfo';

import { getCookieToken } from '@utils/cookie';

interface Auth {
loggedInfo: LoggedInfo;
setLoggedInfo: Dispatch<SetStateAction<LoggedInfo>>;
}

const notLoggedInfo: LoggedInfo = {
isLogged: false,
accessToken: '',
};

export const AuthContext = createContext({} as Auth);

export function AuthProvider({ children }: { children: React.ReactNode }) {
const [loggedInfo, setLoggedInfo] = useState(notLoggedInfo);
const { data: userInfo } = useUserInfo(loggedInfo.isLogged);

useEffect(() => {
if (userInfo) setLoggedInfo(origin => ({ ...origin, userInfo }));
}, [userInfo]);

useEffect(() => {
const accessToken = getCookieToken().accessToken;
if (accessToken) setLoggedInfo(origin => ({ ...origin, accessToken }));
}, []);

return (
<AuthContext.Provider value={{ loggedInfo, setLoggedInfo }}>{children}</AuthContext.Provider>
);
}
4 changes: 2 additions & 2 deletions frontend/src/hooks/query/user/useUserInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { getUserInfo } from '@api/wus/userInfo';

import { QUERY_KEY } from '@constants/queryKey';

export const useUserInfo = () => {
export const useUserInfo = (isLogged: boolean) => {
const { data, error, isLoading, isError } = useQuery<UserInfoResponse>(
[QUERY_KEY.USER_INFO],
[QUERY_KEY.USER_INFO, isLogged],
getUserInfo
);

Expand Down
12 changes: 6 additions & 6 deletions frontend/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import ReactDOM from 'react-dom/client';
import App from './App';
import { worker } from './mocks/worker';

if (process.env.NODE_ENV === 'development') {
worker.start();
}
// if (process.env.NODE_ENV === 'development') {
// worker.start();
// }

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
// <React.StrictMode>
<App />
// </React.StrictMode>
);
60 changes: 60 additions & 0 deletions frontend/src/pages/auth/Login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useNavigate } from 'react-router-dom';

import styled from 'styled-components';

import LogoButton from '@components/common/LogoButton';
import SquareButton from '@components/common/SquareButton';

import kakaoLogin from '@assets/kakao_login_medium_wide.svg';

export default function Login() {
const navigate = useNavigate();
const CLIENT_ID = `${process.env.VOTOGETHER_REST_API_KEY}`;
const REDIRECT_URI = `${process.env.VOTOGETHER_CLIENT_REDIRECT_URL}`;
const kakaoURL = `https://kauth.kakao.com/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code`;

return (
<Wrapper>
<LogoButton content="icon" style={{ width: '100px', height: '100px' }} />
<ButtonWrapper>
<KaKaoLoginImg
alt="카카오 로그인"
src={kakaoLogin}
onClick={() => (window.location.href = kakaoURL)}
/>
<SquareButton onClick={() => navigate('/')} theme="blank" style={{ height: '35px' }}>
비회원으로 이용하기
</SquareButton>
</ButtonWrapper>
</Wrapper>
);
}

const Wrapper = styled.div`
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
gap: 150px;
width: 320px;
height: 1vh;
margin-top: 300px;
position: fixed;
left: 10%;
@media (min-width: 576px) {
left: 30%;
}
`;

const KaKaoLoginImg = styled.img`
width: 255px;
height: 35px;
`;

const ButtonWrapper = styled.div`
display: flex;
flex-direction: column;
justify-content: space-around;
gap: 20px;
width: 230px;
`;
61 changes: 61 additions & 0 deletions frontend/src/pages/auth/Redirection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useContext, useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';

import { AuthResponse } from '@type/auth';

import { AuthContext } from '@hooks/context/auth';

import { getCookieToken, setCookieToken } from '@utils/cookie';
import { getFetch } from '@utils/fetch';

const getAuthInfo = async (url: string): Promise<AuthResponse> => {
return await getFetch<AuthResponse>(url);
};

export default function Redirection() {
const { loggedInfo, setLoggedInfo } = useContext(AuthContext);
const [errorMessage, setErrorMessage] = useState('');
const [isLoading, setIsLoading] = useState(true);
const [params] = useSearchParams();

const navigate = useNavigate();

useEffect(() => {
(async () => {
setIsLoading(true);
setErrorMessage('');

const code = params.get('code');
const REGISTER_API_URL = `${process.env.VOTOGETHER_BASE_URL}/auth/kakao/callback?code=${code}`;

await getAuthInfo(REGISTER_API_URL)
.finally(() => {
setIsLoading(false);
})
.catch(error => {
setErrorMessage(error.message);
})
.then(res => {
if (!res) return setErrorMessage('잘못된 형식의 response');

const { accessToken } = res;
setCookieToken('accessToken', accessToken);

setLoggedInfo({
...loggedInfo,
accessToken: getCookieToken().accessToken,
isLogged: true,
});

navigate('/');
});
})();
}, [navigate, loggedInfo, setLoggedInfo]);

return (
<div>
{isLoading && '로그인 중입니다...'}
{errorMessage && errorMessage}
</div>
);
}
17 changes: 12 additions & 5 deletions frontend/src/routes/router.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { createBrowserRouter } from 'react-router-dom';

import Login from '@pages/auth/Login';
import Redirection from '@pages/auth/Redirection';
import Home from '@pages/Home';
import MyInfo from '@pages/MyInfo';
import CreatePost from '@pages/post/CreatePost';
Expand All @@ -13,10 +15,15 @@ const router = createBrowserRouter([
{
path: PATH.HOME,
element: <Home />,
children: [
{ path: 'search', element: <Home /> },
{ path: 'login', element: <Home /> },
],
children: [{ path: 'search', element: <Home /> }],
},
{
path: PATH.LOGIN,
element: <Login />,
},
{
path: 'auth/kakao/callback',
element: <Redirection />,
},
{
path: PATH.POST,
Expand All @@ -34,7 +41,7 @@ const router = createBrowserRouter([
path: 'result/:postId',
element: <VoteStatisticsPage />,
},
{ path: 'category/:categoryId', element: <Home /> },
{ path: 'posts/category/:categoryId', element: <Home /> },
],
},
{
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/types/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface AuthResponse {
accessToken: string;
}
7 changes: 7 additions & 0 deletions frontend/src/types/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,10 @@ export interface UserInfoResponse {
export interface ModifyNicknameRequest {
nickname: string;
}


export interface LoggedInfo {
accessToken: string;
isLogged: boolean;
userInfo?: UserInfoResponse;
}
17 changes: 17 additions & 0 deletions frontend/src/utils/cookie/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
type CookieKey = 'accessToken' | 'refreshToken';

export const setCookieToken = (key: CookieKey, token: string) => {
//secure 속성은 현재 dev에서는 http로 진행중이기 때문에 사용할 수 없음
document.cookie = `${encodeURIComponent(key)}=${encodeURIComponent(token)}; path=/`;
};

// token형식 = "key=value; key=value; key=value"
export function getCookieToken() {
const cookie = document.cookie;
const cookieContent = {} as { [key: string]: any };
cookie.split('; ').forEach(pair => {
const [key, value] = pair.split('=');
cookieContent[key] = value;
});
return cookieContent as Record<CookieKey, any>;
}
Loading

0 comments on commit 28e2e7c

Please sign in to comment.