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 (
-