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

[FE] 카카오 로그인 기능 추가 #833

Merged
merged 15 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions client/cypress/e2e/createEvent.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ describe('Flow: 랜딩 페이지에서부터 이벤트를 생성 완료하는 fl
it('랜딩페이지에서 "정산 시작하기" 버튼을 눌러 행사 이름 입력 페이지로 이동해야 한다.', () => {
cy.visit('/');
cy.get('button').contains('정산 시작하기').click();
cy.url().should('include', ROUTER_URLS.login);
cy.get('button').contains('비회원으로 진행하기').click();
Comment on lines +14 to +15
Copy link
Contributor

Choose a reason for hiding this comment

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

오~ 잊지 않고 추가! 멋져용

cy.url().should('include', ROUTER_URLS.createEvent);
});

Expand Down
13 changes: 13 additions & 0 deletions client/src/apis/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ export const requestGet = async <T>({
return data;
};

export const requestGetWithoutResponse = async ({
headers = {},
errorHandlingStrategy,
...args
}: WithErrorHandlingStrategy<RequestMethodProps>) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

신경써서 구현해줘서 고맙습니다. 더 편리한 방법을 찾고 싶은데 마땅히 떠오르는게 없네요 ㅜㅜ....

await request({
...args,
method: 'GET',
headers,
errorHandlingStrategy,
});
};

export const requestPatch = ({headers = {}, ...args}: RequestMethodProps) => {
return request({method: 'PATCH', headers, ...args});
};
Expand Down
20 changes: 19 additions & 1 deletion client/src/apis/request/auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {BASE_URL} from '@apis/baseUrl';
import {ADMIN_API_PREFIX, USER_API_PREFIX} from '@apis/endpointPrefix';
import {requestPostWithoutResponse} from '@apis/fetcher';
import {requestGet, requestGetWithoutResponse, requestPostWithoutResponse} from '@apis/fetcher';
import {WithEventId} from '@apis/withId.type';

import getKakaoRedirectUrl from '@utils/getKakaoRedirectUrl';

export const requestPostAuthentication = async ({eventId}: WithEventId) => {
await requestPostWithoutResponse({
baseUrl: BASE_URL.HD,
Expand All @@ -23,3 +25,19 @@ export const requestPostToken = async ({eventId, password}: WithEventId<RequestP
},
});
};

export const requestKakaoClientId = async () => {
return await requestGet<{clientId: string}>({
baseUrl: BASE_URL.HD,
endpoint: '/api/kakao-client-id',
});
};

export const requestGetKakaoLogin = async (code: string) => {
await requestGetWithoutResponse({
Copy link
Contributor

Choose a reason for hiding this comment

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

GET인데 바디가없는 GET도 있었군요!.. 쿠키만 세팅해주고 끝나기 때문인가보네요. 새로 알아갑니다~

Copy link
Contributor

Choose a reason for hiding this comment

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

근데 그럼 또 궁금한게 바디없는 POST로 만들면 안됐던걸까요!?

baseUrl: BASE_URL.HD,
endpoint: `/api/login/kakao?code=${code}&redirect_uri=${getKakaoRedirectUrl()}`,
});

return null;
};
3 changes: 3 additions & 0 deletions client/src/assets/image/kakao.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ const getButtonVariantsStyle = (variants: ButtonVariants, theme: Theme) => {
}),
getHoverAndActiveBackground(theme.colors.tertiary),
],
kakao: [
css({
backgroundColor: theme.colors.kakao,
color: theme.colors.onKakao,
}),
],
};

return style[variants];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Theme} from '@theme/theme.type';

export type ButtonSize = 'small' | 'medium' | 'semiLarge' | 'large';
export type ButtonVariants = 'primary' | 'secondary' | 'tertiary' | 'destructive' | 'loading';
export type ButtonVariants = 'primary' | 'secondary' | 'tertiary' | 'destructive' | 'loading' | 'kakao';

export interface ButtonStyleProps {
variants?: ButtonVariants;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ const getFixedButtonVariantsStyle = (variants: ButtonVariants, theme: Theme) =>
}),
getHoverAndActiveBackground(theme.colors.tertiary),
],
kakao: [
css({
backgroundColor: theme.colors.kakao,
color: theme.colors.onKakao,
}),
],
};

return style[variants];
Expand Down
1 change: 1 addition & 0 deletions client/src/components/Design/components/Icon/Icon.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const ICON_DEFAULT_COLOR: Record<IconType, IconColor> = {
heundeut: 'gray',
photoButton: 'white',
chevronDown: 'tertiary',
kakao: 'onKakao',
};

export const iconStyle = ({iconType, theme, iconColor}: IconStylePropsWithTheme) => {
Expand Down
2 changes: 2 additions & 0 deletions client/src/components/Design/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import InputDelete from '@assets/image/inputDelete.svg';
import Error from '@assets/image/error.svg';
import Confirm from '@assets/image/confirm.svg';
import Kakao from '@assets/image/kakao.svg';
import Trash from '@assets/image/trash.svg';
import TrashMini from '@assets/image/trash_mini.svg';
import Search from '@assets/image/search.svg';
Expand Down Expand Up @@ -41,6 +42,7 @@ export const ICON = {
),
photoButton: <PhotoButton />,
chevronDown: <ChevronDownLarge />,
kakao: <Kakao />,
} as const;

export const Icon = ({iconColor, iconType, ...htmlProps}: IconProps) => {
Expand Down
7 changes: 6 additions & 1 deletion client/src/components/Design/token/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ export type ColorKeys =
| 'errorContainer'
| 'onErrorContainer'
| 'warn'
| 'complete';
| 'complete'
| 'kakao'
| 'onKakao';
export type ColorTokens = Record<ColorKeys, Color>;

// TODO: (@soha) 대괄호 사용에 대해 논의
Expand All @@ -106,6 +108,9 @@ export const COLORS: ColorTokens = {
onErrorContainer: PRIMITIVE_COLORS.pink[300],
warn: PRIMITIVE_COLORS.yellow[400],
complete: PRIMITIVE_COLORS.green[300],

kakao: '#FEE500',
onKakao: '#181600',
Comment on lines +112 to +113
Copy link
Contributor

Choose a reason for hiding this comment

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

조금 번거로울 수 있긴 하지만, 해당 컬러들도 위에 PRIMITIVE_COLORS 처럼 객체로 관리해서 넣어주는게 통일성 있고 좋을 것 같아요!
다만 해당 컬러는 카카오와 같은 회사의 지정 컬러니까 PRIMITIVE_COLORS가 아닌 새로운 객체를 만들어서 넣어주는건 어떠세용?

Copy link
Contributor

Choose a reason for hiding this comment

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

오매. 이전에 토다리가 저와 약간 반대 의견을 내주셔서 수정했던 거군요! 호홍

Copy link
Contributor

Choose a reason for hiding this comment

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

소하의 의견도 좋은 아이디어네요~ BRAND_COLORS 와 같은 것을 만들어서 넣어줘도 되겠네요~

};

export const PRIMARY_COLORS = PRIMITIVE_COLORS.purple;
2 changes: 2 additions & 0 deletions client/src/constants/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const QUERY_KEYS = {
reports: 'reports',
billDetails: 'billDetails',
images: 'images',
kakaoClientId: 'kakao-client-id',
kakaoLogin: 'kakao-login',
};

export default QUERY_KEYS;
1 change: 1 addition & 0 deletions client/src/constants/routerUrls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export const ROUTER_URLS = {
addImages: '/event/:eventId/admin/add-images',
send: 'event/:eventId/:memberId/send',
qrCode: 'event/:eventId/qrcode',
login: '/login',
};
1 change: 1 addition & 0 deletions client/src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ declare namespace NodeJS {
readonly API_BASE_URL: string;
readonly AMPLITUDE_KEY: string;
readonly KAKAO_JAVASCRIPT_KEY: string;
readonly KAKAO_REDIRECT_URI: string;
readonly IMAGE_URL: string;
}
}
20 changes: 20 additions & 0 deletions client/src/hooks/queries/auth/useRequestGetKakaoClientId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {useQuery} from '@tanstack/react-query';

import {requestKakaoClientId} from '@apis/request/auth';

import QUERY_KEYS from '@constants/queryKeys';

const useRequestGetKakaoClientId = () => {
const {refetch, ...rest} = useQuery({
queryKey: [QUERY_KEYS.kakaoClientId],
queryFn: requestKakaoClientId,
enabled: false,
});

return {
requestGetClientId: refetch,
...rest,
};
};

export default useRequestGetKakaoClientId;
22 changes: 22 additions & 0 deletions client/src/hooks/queries/auth/useRequestGetKakaoLogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {useQuery} from '@tanstack/react-query';

import {requestGetKakaoLogin} from '@apis/request/auth';

import QUERY_KEYS from '@constants/queryKeys';

const useRequestGetKakaoLogin = () => {
const code = new URLSearchParams(location.search).get('code');

const {refetch, ...rest} = useQuery({
queryKey: [QUERY_KEYS.kakaoLogin, code],
queryFn: () => requestGetKakaoLogin(code ?? ''),
enabled: false,
});

return {
requestGetKakaoLogin: refetch,
...rest,
};
};

export default useRequestGetKakaoLogin;
6 changes: 4 additions & 2 deletions client/src/hooks/useAmplitude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ const useAmplitude = () => {
});
};

const trackStartCreateEvent = () => {
track('정산 시작하기 버튼 클릭');
const trackStartCreateEvent = ({login}: {login: boolean}) => {
track('정산 시작하기 버튼 클릭', {
login,
});
};

const trackCompleteCreateEvent = (eventUniqueData: EventUniqueData) => {
Expand Down
58 changes: 58 additions & 0 deletions client/src/hooks/useLoginPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {useEffect} from 'react';
import {useLocation, useNavigate} from 'react-router-dom';

import {useAuthStore} from '@store/authStore';

import getKakaoRedirectUrl from '@utils/getKakaoRedirectUrl';

import {ROUTER_URLS} from '@constants/routerUrls';

import useRequestGetKakaoClientId from './queries/auth/useRequestGetKakaoClientId';
import useAmplitude from './useAmplitude';
import useRequestGetKakaoLogin from './queries/auth/useRequestGetKakaoLogin';

const useLoginPage = () => {
const navigate = useNavigate();
const location = useLocation();
const {trackStartCreateEvent} = useAmplitude();
const {updateAuth} = useAuthStore();
const {requestGetKakaoLogin} = useRequestGetKakaoLogin();

const {requestGetClientId} = useRequestGetKakaoClientId();

const goKakaoLogin = async () => {
const queryResult = await requestGetClientId();
const clientId = queryResult.data?.clientId;

const link = `https://kauth.kakao.com/oauth/authorize?client_id=${clientId}&redirect_uri=${getKakaoRedirectUrl()}&response_type=code`;
window.location.href = link;
};

const goNonLoginCreateEvent = () => {
trackStartCreateEvent({login: false});
navigate(ROUTER_URLS.createEvent);
};

useEffect(() => {
if (location.search === '') return;

const code = new URLSearchParams(location.search).get('code');

const kakaoLogin = async () => {
if (code) {
await requestGetKakaoLogin();
updateAuth(true);
Copy link
Contributor

Choose a reason for hiding this comment

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

비회원 관리자와 회원 관리자의 인증여부를 하나의 auth 상태로 관리해도 부작용이 없나요?


// 추후에 업데이트 하는 로직 필요
trackStartCreateEvent({login: true});
navigate(ROUTER_URLS.createEvent);
}
};

kakaoLogin();
}, [location.search]);

return {goKakaoLogin, goNonLoginCreateEvent};
};

export default useLoginPage;
16 changes: 0 additions & 16 deletions client/src/hooks/useMainSection.ts

This file was deleted.

4 changes: 4 additions & 0 deletions client/src/mocks/handlers/authHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export const authHandler = [
return new HttpResponse(null, {status: 200});
}),

http.get(`${MOCK_API_PREFIX}/api/login/kakao`, () => {
return new HttpResponse(null, {status: 200});
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

}),

// POST /api/eventId/login (requestPostToken)
http.post<{eventId: string}, {password: string}>(
`${MOCK_API_PREFIX}${USER_API_PREFIX}/:eventId/login`,
Expand Down
11 changes: 11 additions & 0 deletions client/src/pages/LoginPage/LoginPage.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {css} from '@emotion/react';

import {Theme} from '@components/Design/theme/theme.type';

export const hrStyle = (theme: Theme) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

hr이라는 태그가 있는걸 처음 알았네요 🥹 새로운걸 배웠습니당

css({
width: '100%',
height: 1,
Copy link
Contributor

Choose a reason for hiding this comment

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

오 근데 height에 단위없이 1만 쓰면 어떻게 되나요? 그냥 px붙인 걸로 스타일계산이 되나요?


backgroundColor: theme.colors.tertiary,
});
49 changes: 49 additions & 0 deletions client/src/pages/LoginPage/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Image from '@components/Design/components/Image/Image';
Copy link
Contributor

Choose a reason for hiding this comment

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

혹시 이 페이지만 페이지 이름.tsx가 아니라 index.tsx인 이유가 있나요?


import useLoginPage from '@hooks/useLoginPage';

import {Button, Flex, FunnelLayout, Icon, MainLayout, Text, TopNav, useTheme} from '@components/Design';

import getImageUrl from '@utils/getImageUrl';

import {hrStyle} from './LoginPage.style';

const LOGIN_COMMENT = `로그인을 하면 계좌번호를 저장하고\n이전 행사들을 쉽게 볼 수 있어요.`;

const LoginPage = () => {
const {theme} = useTheme();

const {goKakaoLogin, goNonLoginCreateEvent} = useLoginPage();

return (
<MainLayout backgroundColor="white">
<TopNav>
<TopNav.Item displayName="뒤로가기" noEmphasis routePath="-1" />
</TopNav>
<FunnelLayout>
<Flex flexDirection="column" justifyContent="spaceBetween" height="100%">
<Flex flexDirection="column" justifyContent="center" alignItems="center" gap="1rem" margin="0 0 6rem 0">
<Image src={getImageUrl('heundeut', 'webp')} fallbackSrc={getImageUrl('heundeut', 'png')} width={109} />
<Text size="bodyBold" css={{whiteSpace: 'pre-line', textAlign: 'center'}}>
{LOGIN_COMMENT}
</Text>
</Flex>
<Flex flexDirection="column" gap="1rem" width="100%" padding="0 2rem" paddingInline="auto">
<Button variants="kakao" size="large" onClick={goKakaoLogin}>
<Flex alignItems="center" gap="0.625rem">
<Icon iconType="kakao" />
카카오 로그인
</Flex>
</Button>
<hr css={hrStyle(theme)} />
<Button variants="secondary" size="large" onClick={goNonLoginCreateEvent}>
비회원으로 진행하기
</Button>
</Flex>
</Flex>
</FunnelLayout>
</MainLayout>
);
};

export default LoginPage;
7 changes: 2 additions & 5 deletions client/src/pages/MainPage/MainPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import Image from '@components/Design/components/Image/Image';

import useAmplitude from '@hooks/useAmplitude';
import usePageBackground from '@hooks/usePageBackground';

import getImageUrl from '@utils/getImageUrl';
Expand All @@ -13,16 +12,14 @@ import {backgroundImageStyle, backgroundStyle, mainContainer} from './MainPage.s
import CreatorSection from './Section/CreatorSection/CreatorSection';

const MainPage = () => {
const {trackStartCreateEvent} = useAmplitude();
const {isVisible} = usePageBackground();
return (
<div css={mainContainer}>
<Nav trackStartCreateEvent={trackStartCreateEvent} />
<MainSection trackStartCreateEvent={trackStartCreateEvent} />
<Nav />
<MainSection />
<DescriptionSection />
<FeatureSection />
<CreatorSection />

<div css={backgroundStyle}>
<Image
css={backgroundImageStyle(isVisible)}
Expand Down
Loading
Loading