Skip to content

Commit

Permalink
영감탱 익스텐션 로그인 핸들링 (#615)
Browse files Browse the repository at this point in the history
  • Loading branch information
ddarkr committed Feb 20, 2024
1 parent 6d0e7f2 commit 065238e
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 53 deletions.
11 changes: 7 additions & 4 deletions src/components/common/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<DialogProps>) {
const theme = useTheme();
const [isSSR, setIsSSR] = useState(true);

useEffect(() => {
Expand All @@ -34,7 +37,7 @@ export default function Dialog({
animate="animate"
exit="exit"
>
<motion.div css={dialogCss} variants={defaultFadeInUpVariants}>
<motion.div css={dialogCss(theme, dialogWidth)} variants={defaultFadeInUpVariants}>
<div css={dialogContentWrapperCss}>{children}</div>
<div css={dialogButtonWrapperCss}>{actionButtons}</div>
</motion.div>
Expand All @@ -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};
Expand Down
5 changes: 5 additions & 0 deletions src/constants/localStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 1 addition & 1 deletion src/libs/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
211 changes: 163 additions & 48 deletions src/pages/login/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
<>
<SEO title="로그인" />
<article css={loginCss}>
<div css={navMockupCss} />
<div css={loginIntroCardCss}></div>

<form css={loginFieldSetCss} onSubmit={handleFormSubmitEvent}>
<TextField
type="email"
label={'이메일 아이디'}
placeholder={'이메일을 입력해주세요'}
feedback={email.debouncedValue !== '' ? emailError || <>&nbsp;</> : <>&nbsp;</>}
isSuccess={email.debouncedValue.length > 0 && emailError === ''}
value={email.value}
onChange={email.onChange}
required
alertWhenFocused
/>
<TextField
type="password"
label={'비밀번호'}
placeholder={'영문, 숫자 포함 6자 이상의 비밀번호'}
feedback={password.debouncedValue !== '' ? passwordError || <>&nbsp;</> : <>&nbsp;</>}
isSuccess={password.debouncedValue.length > 0 && passwordError === ''}
value={password.value}
onChange={password.onChange}
required
alertWhenFocused
/>
<CTAButton type={'submit'} disabled={isPending}>
로그인
</CTAButton>
</form>
<GhostButton onClick={() => push('/password')}>비밀번호 찾기</GhostButton>
<div css={signUpTextWrapperCss}>
계정이 없으신가요?{' '}
<GhostButton size={'small'} onClick={() => push('/signup')}>
빠르게 가입하기
</GhostButton>
</div>
<Dialog
isShowing={!userCancelExtensionLogin && canExtensionLogin}
dialogWidth={300}
actionButtons={
<>
<FilledButton
colorType="light"
onClick={() => setUserCancelExtensionLogin(true)}
disabled={isPending}
>
다른 계정
</FilledButton>
<div css={dialogLongButtonCss}>
<FilledButton colorType="dark" onClick={handleExtensionLogin} disabled={isPending}>
익스텐션 계정
</FilledButton>
</div>
</>
}
>
영감탱 익스텐션에 로그인되어 있습니다.
<br />
익스텐션 계정으로 로그인할까요?
</Dialog>
</article>
</>
);
}

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,
Expand Down Expand Up @@ -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 (
<>
<SEO title="로그인" />
<article css={loginCss}>
<div css={navMockupCss} />
<div css={loginIntroCardCss}></div>
return { handleFormSubmitEvent, email, password, emailError, passwordError };
}

<form css={loginFieldSetCss} onSubmit={handleFormSubmitEvent}>
<TextField
type="email"
label={'이메일 아이디'}
placeholder={'이메일을 입력해주세요'}
feedback={email.debouncedValue !== '' ? emailError || <>&nbsp;</> : <>&nbsp;</>}
isSuccess={email.debouncedValue.length > 0 && emailError === ''}
value={email.value}
onChange={email.onChange}
required
alertWhenFocused
/>
<TextField
type="password"
label={'비밀번호'}
placeholder={'영문, 숫자 포함 6자 이상의 비밀번호'}
feedback={password.debouncedValue !== '' ? passwordError || <>&nbsp;</> : <>&nbsp;</>}
isSuccess={password.debouncedValue.length > 0 && passwordError === ''}
value={password.value}
onChange={password.onChange}
required
alertWhenFocused
/>
<CTAButton type={'submit'} disabled={isPending}>
로그인
</CTAButton>
</form>
<GhostButton onClick={() => push('/password')}>비밀번호 찾기</GhostButton>
<div css={signUpTextWrapperCss}>
계정이 없으신가요?{' '}
<GhostButton size={'small'} onClick={() => push('/signup')}>
빠르게 가입하기
</GhostButton>
</div>
</article>
</>
);
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`
Expand Down Expand Up @@ -166,3 +276,8 @@ const signUpTextWrapperCss = (theme: Theme) => css`
font-size: 10px;
line-height: 150%;
`;

const dialogLongButtonCss = css`
width: 160px;
flex-shrink: 0;
`;

0 comments on commit 065238e

Please sign in to comment.