-
Notifications
You must be signed in to change notification settings - Fork 14
feat: 로그인 끊김/실패 시 복구 UX 개선 #387
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
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
c783551
feat: 세션 만료 시 차단형 Dialog와 원래 페이지 복귀 UX
sumi-0011 748bed0
feat: /auth/desktop 비로그인/실패 케이스 UX 대응
sumi-0011 39c69bc
Merge remote-tracking branch 'origin/dev' into feat/login-recovery-ux
sumi-0011 41837c9
feat(web): 만료·비로그인 시 딥링크 자동 복귀
sumi-0011 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| 'use client'; | ||
|
|
||
| import { useEffect, useState } from 'react'; | ||
| import { useRouter, useSearchParams } from 'next/navigation'; | ||
| import { useTranslations } from 'next-intl'; | ||
| import { Button } from '@gitanimals/ui-tailwind'; | ||
|
|
||
| import { login } from '@/components/AuthButton'; | ||
| import { LOCAL_STORAGE_KEY } from '@/constants/storage'; | ||
|
|
||
| const DESKTOP_CALLBACK_HINTS = ['/auth/desktop', 'redirect_uri=']; | ||
|
|
||
| const isDesktopCallback = (value: string | null): value is string => { | ||
| if (!value) return false; | ||
| return DESKTOP_CALLBACK_HINTS.some((hint) => value.includes(hint)); | ||
| }; | ||
|
|
||
| const pageRootClass = | ||
| 'flex min-h-screen flex-col items-center justify-center p-[24px] text-white bg-[linear-gradient(180deg,#000_0%,#004875_38.51%,#005B93_52.46%,#006FB3_73.8%,#0187DB_100%)] mobile:p-[16px]'; | ||
| const cardClass = | ||
| 'flex w-fit min-w-[520px] max-w-full flex-col items-center gap-[16px] rounded-[16px] bg-white-10 p-[40px] backdrop-blur-[7px] mobile:min-w-full mobile:bg-[rgba(255,255,255,0.08)] mobile:px-[16px] mobile:py-[24px]'; | ||
|
|
||
| export default function AuthErrorPage() { | ||
| const params = useSearchParams(); | ||
| const router = useRouter(); | ||
| const t = useTranslations('Auth'); | ||
|
|
||
| const errorCode = params.get('error'); | ||
| const [savedCallbackUrl, setSavedCallbackUrl] = useState<string | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| setSavedCallbackUrl(localStorage.getItem(LOCAL_STORAGE_KEY.callbackUrl)); | ||
| }, []); | ||
|
|
||
| const isDesktopFlow = isDesktopCallback(savedCallbackUrl); | ||
|
|
||
| const description = (() => { | ||
| switch (errorCode) { | ||
| case 'AccessDenied': | ||
| return t('errorDescriptionAccessDenied'); | ||
| case 'CredentialsSignin': | ||
| return t('errorDescriptionCredentials'); | ||
| default: | ||
| return t('errorDescriptionDefault'); | ||
| } | ||
| })(); | ||
|
|
||
| const handleRetry = () => { | ||
| if (isDesktopFlow && savedCallbackUrl) { | ||
| router.replace(savedCallbackUrl); | ||
| return; | ||
| } | ||
| login('/mypage'); | ||
| }; | ||
|
|
||
| const handleGoHome = () => { | ||
| router.replace('/'); | ||
| }; | ||
|
|
||
| return ( | ||
| <main className={pageRootClass}> | ||
| <section className={cardClass}> | ||
| <h1 className="glyph28-bold text-white mobile:glyph24-bold">{t('errorTitle')}</h1> | ||
| <p className="whitespace-pre-line text-center glyph16-regular text-white-75">{description}</p> | ||
| {errorCode && <p className="glyph14-regular text-white-50">code: {errorCode}</p>} | ||
| <div className="mt-[8px] flex flex-wrap justify-center gap-[12px]"> | ||
| <Button variant="primary" size="m" onClick={handleRetry}> | ||
| {isDesktopFlow ? t('errorRetryDesktop') : t('errorRetryDefault')} | ||
| </Button> | ||
| <Button variant="secondary" size="m" onClick={handleGoHome}> | ||
| {t('errorGoHome')} | ||
| </Button> | ||
| </div> | ||
| </section> | ||
| </main> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| 'use client'; | ||
|
|
||
| import { useEffect } from 'react'; | ||
| import { useSearchParams } from 'next/navigation'; | ||
|
|
||
| import { LOCAL_STORAGE_KEY } from '@/constants/storage'; | ||
|
|
||
| // 보호 라우트에서 미들웨어가 홈으로 되돌릴 때 실어 보낸 `?callbackUrl` 을 캡처해 | ||
| // localStorage 에 저장한다. 사용자가 로그인하면 login()/LoginButton 이 이 값을 읽어 | ||
| // 원래 목적지로 자동 복귀시킨다. (useSearchParams 구독으로 클라 네비게이션 유입도 감지) | ||
| export function LoginCallbackWatcher() { | ||
| const searchParams = useSearchParams(); | ||
|
|
||
| useEffect(() => { | ||
| const callbackUrl = searchParams.get('callbackUrl'); | ||
| if (!callbackUrl) return; | ||
|
|
||
| // open-redirect 방지: 외부 절대 URL(`//`, `https://…`)은 무시하고 앱 내부 경로만 허용. | ||
| const isInternalPath = callbackUrl.startsWith('/') && !callbackUrl.startsWith('//'); | ||
| if (isInternalPath) { | ||
| localStorage.setItem(LOCAL_STORAGE_KEY.callbackUrl, callbackUrl); | ||
| } | ||
|
|
||
| const params = new URLSearchParams(searchParams.toString()); | ||
| params.delete('callbackUrl'); | ||
| const query = params.toString(); | ||
| window.history.replaceState(null, '', window.location.pathname + (query ? `?${query}` : '')); | ||
| }, [searchParams]); | ||
|
|
||
| return null; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| 'use client'; | ||
|
|
||
| import { useTranslations } from 'next-intl'; | ||
| import { Button, Dialog } from '@gitanimals/ui-tailwind'; | ||
| import { useAtomValue } from 'jotai'; | ||
|
|
||
| import { login } from '@/components/AuthButton'; | ||
| import { sessionExpiredAtom } from '@/utils/sessionExpired'; | ||
|
|
||
| export function SessionExpiredDialog() { | ||
| const { open, callbackUrl } = useAtomValue(sessionExpiredAtom); | ||
| const t = useTranslations('Auth'); | ||
|
|
||
| const handleLogin = () => { | ||
| login(callbackUrl ?? '/mypage'); | ||
| }; | ||
|
|
||
| return ( | ||
| <Dialog open={open}> | ||
| <Dialog.Content | ||
| isShowClose={false} | ||
| onEscapeKeyDown={(e) => e.preventDefault()} | ||
| onPointerDownOutside={(e) => e.preventDefault()} | ||
| onInteractOutside={(e) => e.preventDefault()} | ||
| > | ||
| <Dialog.Title className="text-left glyph20-regular">{t('sessionExpiredTitle')}</Dialog.Title> | ||
| <Dialog.Description className="w-full text-left glyph16-regular text-white-75"> | ||
| {t('sessionExpiredDescription')} | ||
| </Dialog.Description> | ||
| <div className="flex w-full justify-end gap-[8px]"> | ||
| <Button onClick={handleLogin} variant="primary" size="m"> | ||
| {t('sessionExpiredAction')} | ||
| </Button> | ||
| </div> | ||
| </Dialog.Content> | ||
| </Dialog> | ||
| ); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
서버에서 난 401은 세션 캐시를 비우지 못합니다.
지금은
clearSessionCache()가 브라우저 분기 안에만 있어서, 서버 렌더링/서버 호출에서 401이 발생하면 같은 워커가 최대 5분 동안 만료된 access token을 계속 재사용할 수 있습니다. 캐시 무효화는 환경과 무관하게 먼저 수행하고,triggerSessionExpired()만 클라이언트에서 호출해야 합니다.🔧 제안 수정
export const interceptorResponseRejected = async (error: AxiosError<ApiErrorScheme>) => { if (error?.response?.status === 401) { - if (typeof window !== 'undefined') { - clearSessionCache(); + clearSessionCache(); + if (typeof window !== 'undefined') { triggerSessionExpired(window.location.pathname + window.location.search); } throw new CustomException('TOKEN_EXPIRED', 'token expired'); }📝 Committable suggestion
🤖 Prompt for AI Agents