diff --git a/src/components/common/Dialog.tsx b/src/components/common/Dialog.tsx index 75ee3e1c..2af42ea0 100644 --- a/src/components/common/Dialog.tsx +++ b/src/components/common/Dialog.tsx @@ -1,5 +1,5 @@ import { PropsWithChildren, ReactNode, useEffect, useState } from 'react'; -import { css, Theme } from '@emotion/react'; +import { css, Theme, useTheme } from '@emotion/react'; import { motion } from 'framer-motion'; import PortalWrapper from '~/components/common/PortalWrapper'; @@ -10,13 +10,16 @@ import { dimBackdropCss } from './styles'; export interface DialogProps { isShowing?: boolean; actionButtons: ReactNode; + dialogWidth?: number; } export default function Dialog({ isShowing, children, actionButtons, + dialogWidth, }: PropsWithChildren) { + const theme = useTheme(); const [isSSR, setIsSSR] = useState(true); useEffect(() => { @@ -34,7 +37,7 @@ export default function Dialog({ animate="animate" exit="exit" > - +
{children}
{actionButtons}
@@ -54,14 +57,14 @@ const dimBackdropLayoutCss = (theme: Theme) => css` ${dimBackdropCss(theme)} `; -const dialogCss = (theme: Theme) => css` +const dialogCss = (theme: Theme, width = 311) => css` position: relative; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; - width: 311px; + width: ${width}px; min-height: 200px; background-color: ${theme.color.background}; diff --git a/src/constants/localStorage.ts b/src/constants/localStorage.ts index d872d6c6..15109fcd 100644 --- a/src/constants/localStorage.ts +++ b/src/constants/localStorage.ts @@ -2,3 +2,8 @@ export const localStorageUserTokenKeys = { accessToken: 'ygtlsat', refreshToken: 'ygtrfhtk', } as const; + +export const localStorageExtensionKeys = { + use: 'use-ygtang-extension', + refreshToken: 'ygte-refresh', +} as const; diff --git a/src/libs/api/client.ts b/src/libs/api/client.ts index ce82c774..5e33f072 100644 --- a/src/libs/api/client.ts +++ b/src/libs/api/client.ts @@ -6,7 +6,7 @@ import CustomException from '~/exceptions/CustomException'; import { errorMessage } from '~/exceptions/messages'; import { ApiErrorScheme } from '~/exceptions/type'; -const DEVELOPMENT_API_URL = 'https://api.ygtang.xyz/api'; +const DEVELOPMENT_API_URL = 'https://ygtang.kr/api'; // TODO: 개발 서버 사망에 따른 개발 버전에서도 프로덕션 사용 const PRODUCTION_API_URL = 'https://ygtang.kr/api'; export const instance = axios.create({ diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index 85b92dcb..4298996d 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -1,10 +1,13 @@ import { FormEvent, useEffect, useState } from 'react'; import { css, Theme } from '@emotion/react'; -import { CTAButton, GhostButton } from '~/components/common/Button'; +import { CTAButton, FilledButton, GhostButton } from '~/components/common/Button'; +import Dialog from '~/components/common/Dialog'; import SEO from '~/components/common/SEO'; import TextField from '~/components/common/TextField'; +import { localStorageExtensionKeys } from '~/constants/localStorage'; import useMemberLoginMutation from '~/hooks/api/member/useMemberLoginMutation'; +import useReissueMutation from '~/hooks/api/reissue/useReissueMutation'; import useDidUpdate from '~/hooks/common/useDidUpdate'; import useInput from '~/hooks/common/useInput'; import useInternalRouter from '~/hooks/common/useInternalRouter'; @@ -15,15 +18,99 @@ import { recordEvent } from '~/utils/analytics'; import { validator } from '~/utils/validator'; export default function Login() { + const [isPending, setIsPending] = useState(false); + const { push } = useInternalRouter(); + + const { handleFormSubmitEvent, email, password, emailError, passwordError } = useLoginPage({ + setIsPending, + }); + const { + canExtensionLogin, + handleExtensionLogin, + setUserCancelExtensionLogin, + userCancelExtensionLogin, + } = useExtensionAuth({ setIsPending }); + + return ( + <> + +
+
+
+ +
+   : <> } + isSuccess={email.debouncedValue.length > 0 && emailError === ''} + value={email.value} + onChange={email.onChange} + required + alertWhenFocused + /> +   : <> } + isSuccess={password.debouncedValue.length > 0 && passwordError === ''} + value={password.value} + onChange={password.onChange} + required + alertWhenFocused + /> + + 로그인 + + + push('/password')}>비밀번호 찾기 +
+ 계정이 없으신가요?{' '} + push('/signup')}> + 빠르게 가입하기 + +
+ + setUserCancelExtensionLogin(true)} + disabled={isPending} + > + 다른 계정 + +
+ + 익스텐션 계정 + +
+ + } + > + 영감탱 익스텐션에 로그인되어 있습니다. +
+ 익스텐션 계정으로 로그인할까요? +
+
+ + ); +} + +function useLoginPage({ setIsPending }: { setIsPending: (value: boolean) => void }) { const { fireToast } = useToast(); const email = useInput({}); const password = useInput({}); - const [isPending, setIsPending] = useState(false); - const [emailError, setEmailError] = useState(''); - const [passwordError, setPasswordError] = useState(''); - const { userLogin } = useUser(); const { push } = useInternalRouter(); const { getRedirect, goRedirect } = useLoginRedirect(); + const { userLogin } = useUser(); + + const [emailError, setEmailError] = useState(''); + const [passwordError, setPasswordError] = useState(''); const { mutate: loginMutate, @@ -84,52 +171,75 @@ export default function Login() { setIsPending(false); fireToast({ content: loginMutationError.message ?? '알 수 없는 오류가 발생했습니다.' }); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [fireToast, loginMutationError]); - return ( - <> - -
-
-
+ return { handleFormSubmitEvent, email, password, emailError, passwordError }; +} -
-   : <> } - isSuccess={email.debouncedValue.length > 0 && emailError === ''} - value={email.value} - onChange={email.onChange} - required - alertWhenFocused - /> -   : <> } - isSuccess={password.debouncedValue.length > 0 && passwordError === ''} - value={password.value} - onChange={password.onChange} - required - alertWhenFocused - /> - - 로그인 - - - push('/password')}>비밀번호 찾기 -
- 계정이 없으신가요?{' '} - push('/signup')}> - 빠르게 가입하기 - -
-
- - ); +function useExtensionAuth({ setIsPending }: { setIsPending: (value: boolean) => void }) { + const { getRedirect, goRedirect } = useLoginRedirect(); + const { push } = useInternalRouter(); + const { fireToast } = useToast(); + const { userLogin } = useUser(); + const [canExtensionLogin, setCanExtensionLogin] = useState(false); + const [userCancelExtensionLogin, setUserCancelExtensionLogin] = useState(false); + + const { mutate: reissueMutate } = useReissueMutation({ + onSuccess: ({ data }) => { + userLogin({ + accessToken: data.accessToken, + refreshToken: data.refreshToken, + }); + setIsPending(false); + recordEvent({ action: 'Login', value: '로그인 화면에서 익스텐션 계정으로 로그인' }); + + if (getRedirect()) { + goRedirect(); + } else { + push('/'); + } + }, + onError: () => { + fireToast({ content: '익스텐션 계정으로 로그인하는데 실패했습니다.' }); + setCanExtensionLogin(false); + setIsPending(false); + }, + }); + + const handleExtensionLogin = () => { + setIsPending(true); + const token = localStorage.getItem(localStorageExtensionKeys.refreshToken); + if (token) { + reissueMutate({ refreshToken: token }); + } + }; + + useEffect(() => { + const checkLoginAvailable = () => { + if (userCancelExtensionLogin) { + return; + } + if (localStorage.getItem(localStorageExtensionKeys.refreshToken)) { + setCanExtensionLogin(true); + } else { + setCanExtensionLogin(false); + } + }; + checkLoginAvailable(); + const interval = setInterval(checkLoginAvailable, 3000); + return () => { + clearInterval(interval); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return { + canExtensionLogin, + handleExtensionLogin, + setUserCancelExtensionLogin, + userCancelExtensionLogin, + }; } const navMockupCss = css` @@ -166,3 +276,8 @@ const signUpTextWrapperCss = (theme: Theme) => css` font-size: 10px; line-height: 150%; `; + +const dialogLongButtonCss = css` + width: 160px; + flex-shrink: 0; +`;