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
Changes from 10 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
@@ -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);
});

13 changes: 13 additions & 0 deletions client/src/apis/fetcher.ts
Original file line number Diff line number Diff line change
@@ -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});
};
23 changes: 22 additions & 1 deletion client/src/apis/request/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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';

export const requestPostAuthentication = async ({eventId}: WithEventId) => {
@@ -23,3 +23,24 @@ 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) => {
const redirectUri =
process.env.NODE_ENV === 'development'
? 'http://localhost:3000' + process.env.KAKAO_REDIRECT_URI
: 'https://haengdong.pro' + process.env.KAKAO_REDIRECT_URI;

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=${redirectUri}`,
});

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
@@ -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];
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;
Original file line number Diff line number Diff line change
@@ -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];
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
@@ -20,6 +20,7 @@ const ICON_DEFAULT_COLOR: Record<IconType, IconColor> = {
heundeut: 'gray',
photoButton: 'white',
chevronDown: 'tertiary',
kakao: 'kakao',
Copy link
Contributor

Choose a reason for hiding this comment

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

@assets/image/kakao.svg에서 comment를 단 것 처럼, currentColor로 변경해 주지 않았기 때문에, IconComponent에서 iconColor를 prop으로 받아도 아마 변경되지 않을거에요

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fill currentColor로 설정되어야 여기서 default 색상을 넣었을 때 적용이 되고 나중에 iconColor를 넣었을 때 그 값이 적용된다! 감사합니다~

};

export const iconStyle = ({iconType, theme, iconColor}: IconStylePropsWithTheme) => {
2 changes: 2 additions & 0 deletions client/src/components/Design/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
@@ -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';
@@ -41,6 +42,7 @@ export const ICON = {
),
photoButton: <PhotoButton />,
chevronDown: <ChevronDownLarge />,
kakao: <Kakao />,
} as const;

export const Icon = ({iconColor, iconType, ...htmlProps}: IconProps) => {
9 changes: 8 additions & 1 deletion client/src/components/Design/token/colors.ts
Original file line number Diff line number Diff line change
@@ -35,6 +35,7 @@ const PRIMITIVE_COLORS = {
700: '#c9d323',
800: '#b9bb17',
900: '#9e9305',
kakao: '#FEE500',
},
green: {
50: '#f4ffe8',
@@ -59,6 +60,7 @@ const PRIMITIVE_COLORS = {
700: '#56555A',
800: '#38373B',
900: '#18171B',
kakao: '#181600',
},
};

@@ -81,7 +83,9 @@ export type ColorKeys =
| 'errorContainer'
| 'onErrorContainer'
| 'warn'
| 'complete';
| 'complete'
| 'kakao'
| 'onKakao';
export type ColorTokens = Record<ColorKeys, Color>;

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

kakao: PRIMITIVE_COLORS.yellow['kakao'],
Copy link
Contributor

Choose a reason for hiding this comment

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

음... PRIMITIVE에 별도로 값을 추가하지 않고, SEMENTIC에 바로 값을 넣어주는 것이 좋아보여요!
RPIMITIVE에 kakao라는 키가 있는것이 어색하기도 하고, 의미상 맞지 않아요 ㅜㅜ

Copy link
Contributor Author

Choose a reason for hiding this comment

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

이거 어떻게 넣어야할까 고민했었는데 바로 SEMENTIC에 넣어도 되겠군요! 좋습니다

onKakao: PRIMITIVE_COLORS.gray['kakao'],
};

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
@@ -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
@@ -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
@@ -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
@@ -36,8 +36,10 @@ const useAmplitude = () => {
});
};

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

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

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

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 redirectUri =
Copy link
Contributor

Choose a reason for hiding this comment

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

auth.ts에서도 동일하게 사용하던데 하나의 constants 파일에서 관리하는 건 어떤가용?

process.env.NODE_ENV === 'development'
? 'http://localhost:3000' + process.env.KAKAO_REDIRECT_URI
: 'https://haengdong.pro' + process.env.KAKAO_REDIRECT_URI;
Copy link
Contributor

Choose a reason for hiding this comment

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

저희 app.haengdong.pro도 있긴한데, 그냥 haengdong.pro로 리다이렉션해도 괜찮겠죠?
그리고, dev.haengdong.pro에서는 어떻게 되는지 궁금해용

Copy link
Contributor Author

Choose a reason for hiding this comment

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

이게 개발 환경에서 테스트할 때는 localhost:3000이라서 이렇게 해뒀어요. cypress 테스트를 할 때도 localhost:3000으로 열리니깐 문제 없다고 생각했습니다!

prod는 고민이 되네요... 근데 크게 무리는 없을 것 같아요


const link = `https://kauth.kakao.com/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code`;
Copy link
Contributor

Choose a reason for hiding this comment

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

얘도 마찬가지로 나중에 관리할 것을 생각해서 constnats 파일에 따로 분리해두는 것도 좋을 것 같아용

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
@@ -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`,
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,
});
47 changes: 47 additions & 0 deletions client/src/pages/LoginPage/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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 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'}}>
{`로그인을 사용하면\n더 편하게 사용할 수 있어요`}
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 Author

Choose a reason for hiding this comment

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

그 내용을 직접 표현해도 되겠네요!
로그인 장점이 자주 사용하는 계좌번호 등록, 내가 만든 행사 열람, 행사 비밀번호 플로우 생략인데 이를 더 표현해봐도 좋을 것 같아요. 이를 어떻게 다 담을 수 있을지는 고민해볼게요!

</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;
Loading
Loading