From 67170a9fb0d9ce56c9e7b3910187385b6667c357 Mon Sep 17 00:00:00 2001 From: JinHo Kim <81083461+jinhokim98@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:45:27 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#833)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: kakao 이미지 추가 * feat: 정산 시작하기 로그인 페이지로 이동 및 로그인 페이지 제작 * feat: 카카오 로그인 기능 구현 * feat: Amplitude 로그인 한 유저와 아닌 유저 구분하는 기능 구현 * fix: get 응답이 없는 함수를 생성해서 json 오류가 나지 않도록 설정 * test: 카카오 로그인 핸들러 설정 * fix: redirect 설정오류 수정 * feat: tanstack query로 카카오 로그인 불러서 에러처리 되도록 설정 * feat: 이미지 width 설정해서 layout shift 생기지 않도록 설정 * fix: 정산 시작하기 버튼 누를 때 플로우가 바뀌어 cypress 코드 수정 * style: primitive kakao 값 semantic으로 이동 * fix: svg fill currentColor로 변경해서 Icon 컴포넌트에서 색상을 넣어줄 수 있도록 변경 * fix: 카카오 아이콘 색상 변경 * feat: 로그인 유도 멘트 변경 * refactor: redirect uri 도메인 오리진을 얻어서 붙이는 방식으로 변경 --- client/cypress/e2e/createEvent.cy.ts | 2 + client/src/apis/fetcher.ts | 13 +++++ client/src/apis/request/auth.ts | 20 ++++++- client/src/assets/image/kakao.svg | 3 + .../Design/components/Button/Button.style.ts | 6 ++ .../Design/components/Button/Button.type.ts | 2 +- .../FixedButton/FixedButton.style.ts | 6 ++ .../Design/components/Icon/Icon.style.ts | 1 + .../Design/components/Icon/Icon.tsx | 2 + client/src/components/Design/token/colors.ts | 7 ++- client/src/constants/queryKeys.ts | 2 + client/src/constants/routerUrls.ts | 1 + client/src/global.d.ts | 1 + .../auth/useRequestGetKakaoClientId.ts | 20 +++++++ .../queries/auth/useRequestGetKakaoLogin.ts | 22 +++++++ client/src/hooks/useAmplitude.ts | 6 +- client/src/hooks/useLoginPage.ts | 58 +++++++++++++++++++ client/src/hooks/useMainSection.ts | 16 ----- client/src/mocks/handlers/authHandlers.ts | 4 ++ client/src/pages/LoginPage/LoginPage.style.ts | 11 ++++ client/src/pages/LoginPage/index.tsx | 49 ++++++++++++++++ client/src/pages/MainPage/MainPage.tsx | 7 +-- client/src/pages/MainPage/Nav/Nav.tsx | 13 ++--- .../Section/MainSection/MainSection.tsx | 16 ++--- client/src/router.tsx | 5 ++ client/src/utils/getKakaoRedirectUrl.ts | 5 ++ 26 files changed, 256 insertions(+), 42 deletions(-) create mode 100644 client/src/assets/image/kakao.svg create mode 100644 client/src/hooks/queries/auth/useRequestGetKakaoClientId.ts create mode 100644 client/src/hooks/queries/auth/useRequestGetKakaoLogin.ts create mode 100644 client/src/hooks/useLoginPage.ts delete mode 100644 client/src/hooks/useMainSection.ts create mode 100644 client/src/pages/LoginPage/LoginPage.style.ts create mode 100644 client/src/pages/LoginPage/index.tsx create mode 100644 client/src/utils/getKakaoRedirectUrl.ts diff --git a/client/cypress/e2e/createEvent.cy.ts b/client/cypress/e2e/createEvent.cy.ts index 30e50b4f5..5a36a74ef 100644 --- a/client/cypress/e2e/createEvent.cy.ts +++ b/client/cypress/e2e/createEvent.cy.ts @@ -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(); cy.url().should('include', ROUTER_URLS.createEvent); }); diff --git a/client/src/apis/fetcher.ts b/client/src/apis/fetcher.ts index 35671b167..4a4f23f2b 100644 --- a/client/src/apis/fetcher.ts +++ b/client/src/apis/fetcher.ts @@ -63,6 +63,19 @@ export const requestGet = async ({ return data; }; +export const requestGetWithoutResponse = async ({ + headers = {}, + errorHandlingStrategy, + ...args +}: WithErrorHandlingStrategy) => { + await request({ + ...args, + method: 'GET', + headers, + errorHandlingStrategy, + }); +}; + export const requestPatch = ({headers = {}, ...args}: RequestMethodProps) => { return request({method: 'PATCH', headers, ...args}); }; diff --git a/client/src/apis/request/auth.ts b/client/src/apis/request/auth.ts index aac5c622d..4f2486b5d 100644 --- a/client/src/apis/request/auth.ts +++ b/client/src/apis/request/auth.ts @@ -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, @@ -23,3 +25,19 @@ export const requestPostToken = async ({eventId, password}: WithEventId { + return await requestGet<{clientId: string}>({ + baseUrl: BASE_URL.HD, + endpoint: '/api/kakao-client-id', + }); +}; + +export const requestGetKakaoLogin = async (code: string) => { + await requestGetWithoutResponse({ + baseUrl: BASE_URL.HD, + endpoint: `/api/login/kakao?code=${code}&redirect_uri=${getKakaoRedirectUrl()}`, + }); + + return null; +}; diff --git a/client/src/assets/image/kakao.svg b/client/src/assets/image/kakao.svg new file mode 100644 index 000000000..299844ff3 --- /dev/null +++ b/client/src/assets/image/kakao.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/components/Design/components/Button/Button.style.ts b/client/src/components/Design/components/Button/Button.style.ts index e17c9db92..9a4ebe26c 100644 --- a/client/src/components/Design/components/Button/Button.style.ts +++ b/client/src/components/Design/components/Button/Button.style.ts @@ -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]; diff --git a/client/src/components/Design/components/Button/Button.type.ts b/client/src/components/Design/components/Button/Button.type.ts index 512e81105..9c31990d7 100644 --- a/client/src/components/Design/components/Button/Button.type.ts +++ b/client/src/components/Design/components/Button/Button.type.ts @@ -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; diff --git a/client/src/components/Design/components/FixedButton/FixedButton.style.ts b/client/src/components/Design/components/FixedButton/FixedButton.style.ts index 3698c2b52..2dfff4a72 100644 --- a/client/src/components/Design/components/FixedButton/FixedButton.style.ts +++ b/client/src/components/Design/components/FixedButton/FixedButton.style.ts @@ -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]; diff --git a/client/src/components/Design/components/Icon/Icon.style.ts b/client/src/components/Design/components/Icon/Icon.style.ts index 1ffd6f473..4b70b918c 100644 --- a/client/src/components/Design/components/Icon/Icon.style.ts +++ b/client/src/components/Design/components/Icon/Icon.style.ts @@ -20,6 +20,7 @@ const ICON_DEFAULT_COLOR: Record = { heundeut: 'gray', photoButton: 'white', chevronDown: 'tertiary', + kakao: 'onKakao', }; export const iconStyle = ({iconType, theme, iconColor}: IconStylePropsWithTheme) => { diff --git a/client/src/components/Design/components/Icon/Icon.tsx b/client/src/components/Design/components/Icon/Icon.tsx index 4f55abcac..d13f6da2d 100644 --- a/client/src/components/Design/components/Icon/Icon.tsx +++ b/client/src/components/Design/components/Icon/Icon.tsx @@ -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: , chevronDown: , + kakao: , } as const; export const Icon = ({iconColor, iconType, ...htmlProps}: IconProps) => { diff --git a/client/src/components/Design/token/colors.ts b/client/src/components/Design/token/colors.ts index 38eea4de7..a4c50a831 100644 --- a/client/src/components/Design/token/colors.ts +++ b/client/src/components/Design/token/colors.ts @@ -81,7 +81,9 @@ export type ColorKeys = | 'errorContainer' | 'onErrorContainer' | 'warn' - | 'complete'; + | 'complete' + | 'kakao' + | 'onKakao'; export type ColorTokens = Record; // TODO: (@soha) 대괄호 사용에 대해 논의 @@ -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', }; export const PRIMARY_COLORS = PRIMITIVE_COLORS.purple; diff --git a/client/src/constants/queryKeys.ts b/client/src/constants/queryKeys.ts index 0c7d4e50b..044d1a630 100644 --- a/client/src/constants/queryKeys.ts +++ b/client/src/constants/queryKeys.ts @@ -6,6 +6,8 @@ const QUERY_KEYS = { reports: 'reports', billDetails: 'billDetails', images: 'images', + kakaoClientId: 'kakao-client-id', + kakaoLogin: 'kakao-login', }; export default QUERY_KEYS; diff --git a/client/src/constants/routerUrls.ts b/client/src/constants/routerUrls.ts index af3305706..a28cd4525 100644 --- a/client/src/constants/routerUrls.ts +++ b/client/src/constants/routerUrls.ts @@ -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', }; diff --git a/client/src/global.d.ts b/client/src/global.d.ts index 8b3136371..7dc2133bc 100644 --- a/client/src/global.d.ts +++ b/client/src/global.d.ts @@ -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; } } diff --git a/client/src/hooks/queries/auth/useRequestGetKakaoClientId.ts b/client/src/hooks/queries/auth/useRequestGetKakaoClientId.ts new file mode 100644 index 000000000..cb972504b --- /dev/null +++ b/client/src/hooks/queries/auth/useRequestGetKakaoClientId.ts @@ -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; diff --git a/client/src/hooks/queries/auth/useRequestGetKakaoLogin.ts b/client/src/hooks/queries/auth/useRequestGetKakaoLogin.ts new file mode 100644 index 000000000..5924b393e --- /dev/null +++ b/client/src/hooks/queries/auth/useRequestGetKakaoLogin.ts @@ -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; diff --git a/client/src/hooks/useAmplitude.ts b/client/src/hooks/useAmplitude.ts index fd13c75ca..4f314f1f9 100644 --- a/client/src/hooks/useAmplitude.ts +++ b/client/src/hooks/useAmplitude.ts @@ -36,8 +36,10 @@ const useAmplitude = () => { }); }; - const trackStartCreateEvent = () => { - track('정산 시작하기 버튼 클릭'); + const trackStartCreateEvent = ({login}: {login: boolean}) => { + track('정산 시작하기 버튼 클릭', { + login, + }); }; const trackCompleteCreateEvent = (eventUniqueData: EventUniqueData) => { diff --git a/client/src/hooks/useLoginPage.ts b/client/src/hooks/useLoginPage.ts new file mode 100644 index 000000000..247991f3d --- /dev/null +++ b/client/src/hooks/useLoginPage.ts @@ -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); + + // 추후에 업데이트 하는 로직 필요 + trackStartCreateEvent({login: true}); + navigate(ROUTER_URLS.createEvent); + } + }; + + kakaoLogin(); + }, [location.search]); + + return {goKakaoLogin, goNonLoginCreateEvent}; +}; + +export default useLoginPage; diff --git a/client/src/hooks/useMainSection.ts b/client/src/hooks/useMainSection.ts deleted file mode 100644 index 9503674bc..000000000 --- a/client/src/hooks/useMainSection.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {useNavigate} from 'react-router-dom'; - -import {ROUTER_URLS} from '@constants/routerUrls'; - -const useMainSection = (trackStartCreateEvent: () => void) => { - const navigate = useNavigate(); - - const handleClick = () => { - trackStartCreateEvent(); - navigate(ROUTER_URLS.createEvent); - }; - - return {handleClick}; -}; - -export default useMainSection; diff --git a/client/src/mocks/handlers/authHandlers.ts b/client/src/mocks/handlers/authHandlers.ts index 00000b1e8..dc5d49c6e 100644 --- a/client/src/mocks/handlers/authHandlers.ts +++ b/client/src/mocks/handlers/authHandlers.ts @@ -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}); + }), + // POST /api/eventId/login (requestPostToken) http.post<{eventId: string}, {password: string}>( `${MOCK_API_PREFIX}${USER_API_PREFIX}/:eventId/login`, diff --git a/client/src/pages/LoginPage/LoginPage.style.ts b/client/src/pages/LoginPage/LoginPage.style.ts new file mode 100644 index 000000000..9f27351bf --- /dev/null +++ b/client/src/pages/LoginPage/LoginPage.style.ts @@ -0,0 +1,11 @@ +import {css} from '@emotion/react'; + +import {Theme} from '@components/Design/theme/theme.type'; + +export const hrStyle = (theme: Theme) => + css({ + width: '100%', + height: 1, + + backgroundColor: theme.colors.tertiary, + }); diff --git a/client/src/pages/LoginPage/index.tsx b/client/src/pages/LoginPage/index.tsx new file mode 100644 index 000000000..8747b0b2f --- /dev/null +++ b/client/src/pages/LoginPage/index.tsx @@ -0,0 +1,49 @@ +import Image from '@components/Design/components/Image/Image'; + +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 ( + + + + + + + + + + {LOGIN_COMMENT} + + + + +
+ +
+
+
+
+ ); +}; + +export default LoginPage; diff --git a/client/src/pages/MainPage/MainPage.tsx b/client/src/pages/MainPage/MainPage.tsx index 87c4ed344..af1bec564 100644 --- a/client/src/pages/MainPage/MainPage.tsx +++ b/client/src/pages/MainPage/MainPage.tsx @@ -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'; @@ -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 (
-