diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx
index 356f1868..101cf712 100644
--- a/apps/admin/src/App.tsx
+++ b/apps/admin/src/App.tsx
@@ -34,6 +34,7 @@ import SitePolicyPage from './pages/SitePolicyPage/SitePolicyPage';
import GiftRegisterPage from './pages/GiftRegisterPage';
import GiftIntroPage from './pages/GiftIntroPage';
import { useAuthAtom } from './atoms/useAuthAtom';
+import GlobalErrorBoundary from './components/ErrorBoundary/GlobalErrorBoundary';
setDefaultOptions({ locale: ko });
@@ -138,6 +139,7 @@ const routes: RouteObject[] = [
),
+ errorElement: ,
children: [...publicRoutes, ...privateRoutes],
},
];
diff --git a/apps/admin/src/atoms/useAuthAtom.ts b/apps/admin/src/atoms/useAuthAtom.ts
index 090f958d..2506c029 100644
--- a/apps/admin/src/atoms/useAuthAtom.ts
+++ b/apps/admin/src/atoms/useAuthAtom.ts
@@ -1,6 +1,5 @@
import { LOCAL_STORAGE } from '@boolti/api';
-import { useAtom } from 'jotai';
-import { atomWithStorage, RESET } from 'jotai/utils';
+import { atom, useAtom } from 'jotai';
const storageMethod = {
getItem: (key: string, initialValue: string | null) => {
@@ -13,15 +12,11 @@ const storageMethod = {
window.localStorage.removeItem(key);
},
};
-const accessTokenAtom = atomWithStorage(
- LOCAL_STORAGE.ACCESS_TOKEN,
- window.localStorage.getItem(LOCAL_STORAGE.ACCESS_TOKEN),
- storageMethod,
+const accessTokenAtom = atom(
+ storageMethod.getItem(LOCAL_STORAGE.ACCESS_TOKEN, null),
);
-const refreshTokenAtom = atomWithStorage(
- LOCAL_STORAGE.REFRESH_TOKEN,
- window.localStorage.getItem(LOCAL_STORAGE.REFRESH_TOKEN),
- storageMethod,
+const refreshTokenAtom = atom(
+ storageMethod.getItem(LOCAL_STORAGE.REFRESH_TOKEN, null),
);
export const useAuthAtom = () => {
@@ -29,13 +24,17 @@ export const useAuthAtom = () => {
const [refreshToken, setRefreshToken] = useAtom(refreshTokenAtom);
const setToken = (accessToken: string, refreshToken: string) => {
+ storageMethod.setItem(LOCAL_STORAGE.ACCESS_TOKEN, accessToken);
+ storageMethod.setItem(LOCAL_STORAGE.REFRESH_TOKEN, refreshToken);
setAccessToken(accessToken);
setRefreshToken(refreshToken);
};
const removeToken = () => {
- setAccessToken(RESET);
- setRefreshToken(RESET);
+ storageMethod.removeItem(LOCAL_STORAGE.ACCESS_TOKEN);
+ storageMethod.removeItem(LOCAL_STORAGE.REFRESH_TOKEN);
+ setAccessToken(null);
+ setRefreshToken(null);
};
const isLogin = () => !!accessToken && !!refreshToken;
diff --git a/apps/admin/src/components/AccountDeleteForm/index.tsx b/apps/admin/src/components/AccountDeleteForm/index.tsx
index 4d726c08..29377602 100644
--- a/apps/admin/src/components/AccountDeleteForm/index.tsx
+++ b/apps/admin/src/components/AccountDeleteForm/index.tsx
@@ -1,7 +1,7 @@
import { Button } from '@boolti/ui';
import Styled from './AccountDeleteForm.styles';
import { useForm } from 'react-hook-form';
-import { useDeleteMe, useLogout } from '@boolti/api';
+import { queryKeys, useDeleteMe, useLogout, useQueryClient } from '@boolti/api';
import { useAuthAtom } from '~/atoms/useAuthAtom';
import { useNavigate } from 'react-router-dom';
import { PATH } from '~/constants/routes';
@@ -20,11 +20,8 @@ const AccountDeleteForm = ({ oauthType, onClose }: AccountDeleteFormProps) => {
const deleteMeMutation = useDeleteMe();
const { removeToken } = useAuthAtom();
- const logoutMutation = useLogout({
- onSuccess: () => {
- removeToken();
- },
- });
+ const queryClient = useQueryClient();
+ const logoutMutation = useLogout();
const {
register,
handleSubmit,
@@ -41,12 +38,16 @@ const AccountDeleteForm = ({ oauthType, onClose }: AccountDeleteFormProps) => {
appleIdAuthorizationCode = appleAuthData?.authorization.code;
}
- await deleteMeMutation.mutateAsync({
+ deleteMeMutation.mutate({
reason: data.reason,
appleIdAuthorizationCode,
});
+
await logoutMutation.mutateAsync();
+ removeToken();
+ queryClient.removeQueries({ ...queryKeys.user.summary });
+
onClose();
navigate(PATH.INDEX);
};
diff --git a/apps/admin/src/components/ErrorBoundary/GlobalErrorBoundary.tsx b/apps/admin/src/components/ErrorBoundary/GlobalErrorBoundary.tsx
new file mode 100644
index 00000000..9bce1ccc
--- /dev/null
+++ b/apps/admin/src/components/ErrorBoundary/GlobalErrorBoundary.tsx
@@ -0,0 +1,23 @@
+import { isBooltiHTTPError } from '@boolti/api/src/BooltiHTTPError';
+import { useEffect } from 'react';
+import { Navigate, useRouteError } from 'react-router-dom';
+import { PATH } from '~/constants/routes';
+
+const GlobalErrorBoundary = () => {
+ const error = useRouteError();
+
+ useEffect(() => {
+ if (error instanceof Error && isBooltiHTTPError(error)) {
+ const errorMessage = '[BooltiHTTPError] errorTraceId:' + error.errorTraceId + '\n';
+ '[BooltiHTTPError] type' + error.type + '\n';
+ '[BooltiHTTPError] detail' + error.detail;
+ console.error(errorMessage);
+ return;
+ }
+ console.error(error);
+ });
+
+ return ;
+};
+
+export default GlobalErrorBoundary;
diff --git a/apps/admin/src/components/ProfileDropdown/ProfileDropdown.styles.ts b/apps/admin/src/components/ProfileDropdown/ProfileDropdown.styles.ts
index 66dbb6b5..80ae945e 100644
--- a/apps/admin/src/components/ProfileDropdown/ProfileDropdown.styles.ts
+++ b/apps/admin/src/components/ProfileDropdown/ProfileDropdown.styles.ts
@@ -21,6 +21,12 @@ const UserProfileImageWrapper = styled.div`
}
`;
+const UserProfileIconWrapper = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+`;
+
const UserProfileImage = styled.img`
border-radius: 50%;
width: 100%;
@@ -63,6 +69,7 @@ const DropdownMenuItemButton = styled.button`
`;
export default {
+ UserProfileIconWrapper,
DropdownContainer,
UserProfileImageWrapper,
UserProfileImage,
diff --git a/apps/admin/src/components/ProfileDropdown/index.tsx b/apps/admin/src/components/ProfileDropdown/index.tsx
index 7eed03bf..71aa0675 100644
--- a/apps/admin/src/components/ProfileDropdown/index.tsx
+++ b/apps/admin/src/components/ProfileDropdown/index.tsx
@@ -1,4 +1,4 @@
-import { useLogout } from '@boolti/api';
+import { queryKeys, useLogout, useQueryClient } from '@boolti/api';
import { ChevronDownIcon, ChevronUpIcon, LogoutIcon, SettingIcon } from '@boolti/icon';
import { useConfirm, useDialog, useDropdown } from '@boolti/ui/src/hooks';
import { useNavigate } from 'react-router-dom';
@@ -39,11 +39,8 @@ const ProfileSVG = () => (
const ProfileDropdown = ({ image, open, disabledDropdown, onClick }: ProfileDropdownProps) => {
const { isOpen, toggleDropdown } = useDropdown();
const { removeToken } = useAuthAtom();
- const logoutMutation = useLogout({
- onSuccess: () => {
- removeToken();
- },
- });
+ const queryClient = useQueryClient();
+ const logoutMutation = useLogout();
const navigate = useNavigate();
const confirm = useConfirm();
const settingDialog = useDialog();
@@ -63,7 +60,9 @@ const ProfileDropdown = ({ image, open, disabledDropdown, onClick }: ProfileDrop
{image ? : }
- {dropdownOpen ? : }
+
+ {dropdownOpen ? : }
+
{dropdownOpen && !disabledDropdown && (
{
const { removeToken } = useAuthAtom();
- const logoutMutation = useLogout({
- onSuccess: () => {
- removeToken();
- },
- });
+ const logoutMutation = useLogout();
+ const queryClient = useQueryClient();
const settingDialog = useDialog();
const confirm = useConfirm();
@@ -98,6 +102,10 @@ const HomePage = () => {
});
if (result) {
await logoutMutation.mutateAsync();
+
+ removeToken();
+ queryClient.removeQueries({ ...queryKeys.user.summary });
+
navigate(PATH.INDEX, { replace: true });
}
}}
diff --git a/apps/admin/src/pages/Landing/components/Header/Header.styles.ts b/apps/admin/src/pages/Landing/components/Header/Header.styles.ts
index 745dd0db..57492b8b 100644
--- a/apps/admin/src/pages/Landing/components/Header/Header.styles.ts
+++ b/apps/admin/src/pages/Landing/components/Header/Header.styles.ts
@@ -106,7 +106,7 @@ const InternalLink = styled(Link)`
const DropDownContainer = styled.div`
margin-left: auto;
- svg {
+ .icon-wrapper > svg {
color: ${({ theme }) => theme.palette.grey.w};
}
`;
diff --git a/apps/admin/src/pages/Landing/components/Header/index.tsx b/apps/admin/src/pages/Landing/components/Header/index.tsx
index 1a0d66d3..9b6bf285 100644
--- a/apps/admin/src/pages/Landing/components/Header/index.tsx
+++ b/apps/admin/src/pages/Landing/components/Header/index.tsx
@@ -1,4 +1,4 @@
-import { useLogout, useUserSummary } from '@boolti/api';
+import { queryKeys, useLogout, useQueryClient, useUserSummary } from '@boolti/api';
import { BooltiDark, CloseIcon, MenuIcon } from '@boolti/icon';
import { useTheme } from '@emotion/react';
import { useAtomValue } from 'jotai';
@@ -26,12 +26,9 @@ const Header = () => {
const theme = useTheme();
const isMobile = deviceWidth < parseInt(theme.breakpoint.mobile, 10);
- const logout = useLogout({
- onSuccess: () => {
- removeToken();
- },
- });
+ const logoutMutation = useLogout();
const navigate = useNavigate();
+ const queryClient = useQueryClient();
const [isExpanded, setIsExpanded] = useState(false);
const { data } = useUserSummary({ enabled: isLogin() });
@@ -39,7 +36,11 @@ const Header = () => {
const onClickAuthButton = async () => {
if (isLogin()) {
- await logout.mutateAsync();
+ await logoutMutation.mutateAsync();
+
+ removeToken();
+ queryClient.removeQueries({ ...queryKeys.user.summary });
+
return;
}
diff --git a/packages/api/src/QueryClientProvider.tsx b/packages/api/src/QueryClientProvider.tsx
index efa664e0..cb4a6c5a 100644
--- a/packages/api/src/QueryClientProvider.tsx
+++ b/packages/api/src/QueryClientProvider.tsx
@@ -11,6 +11,7 @@ export function QueryClientProvider({ children }: React.PropsWithChildren) {
queries: {
refetchOnWindowFocus: false,
retry: false,
+ staleTime: 5000,
useErrorBoundary: (error) => {
// 인증 관련 에러일 때만 ErrorBoundary를 사용한다.
return (
diff --git a/packages/api/src/fetcher.ts b/packages/api/src/fetcher.ts
index d6deffa0..dcc8d01c 100644
--- a/packages/api/src/fetcher.ts
+++ b/packages/api/src/fetcher.ts
@@ -43,7 +43,7 @@ export const instance = ky.create({
afterResponse: [
async (request, options, response) => {
// access token이 만료되었을 때, refresh token으로 새로운 access token을 발급받는다.
- if (!response.ok && response.status === 401) {
+ if (!response.ok && response.status === 401 && !request.url.includes('logout')) {
try {
const { accessToken, refreshToken } = await postRefreshToken();
diff --git a/packages/api/src/mutations/useKakaoToken.ts b/packages/api/src/mutations/useKakaoToken.ts
index 18cd95fc..e6eda7c0 100644
--- a/packages/api/src/mutations/useKakaoToken.ts
+++ b/packages/api/src/mutations/useKakaoToken.ts
@@ -35,6 +35,8 @@ const postKakaoToken = ({ code }: PostKakaoTokenRequest) => {
};
const useKakaoToken = () =>
- useMutation(({ code }: PostKakaoTokenRequest) => postKakaoToken({ code }), { retry: false });
+ useMutation(({ code }: PostKakaoTokenRequest) => postKakaoToken({ code }), {
+ retry: false,
+ });
export default useKakaoToken;
diff --git a/packages/api/src/mutations/useLogout.ts b/packages/api/src/mutations/useLogout.ts
index a2fc6e27..8543248d 100644
--- a/packages/api/src/mutations/useLogout.ts
+++ b/packages/api/src/mutations/useLogout.ts
@@ -1,22 +1,11 @@
-import type { UseMutationOptions } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
-import { LOCAL_STORAGE } from '../constants';
import { fetcher } from '../fetcher';
const postLogout = () => fetcher.post('web/v1/logout');
-const useLogout = (options?: UseMutationOptions) =>
- useMutation(postLogout, {
- ...options,
- onSuccess: (data, variables, context) => {
- if (options?.onSuccess) {
- options.onSuccess(data, variables, context);
- } else {
- window.localStorage.removeItem(LOCAL_STORAGE.ACCESS_TOKEN);
- window.localStorage.removeItem(LOCAL_STORAGE.REFRESH_TOKEN);
- }
- },
- });
+const useLogout = () => {
+ return useMutation({ mutationFn: postLogout });
+};
export default useLogout;
diff --git a/packages/api/src/queries/useUserSummary.ts b/packages/api/src/queries/useUserSummary.ts
index 53263029..d80bf1d7 100644
--- a/packages/api/src/queries/useUserSummary.ts
+++ b/packages/api/src/queries/useUserSummary.ts
@@ -3,6 +3,6 @@ import { useQuery } from '@tanstack/react-query';
import { queryKeys } from '../queryKey';
const useUserSummary = ({ enabled }: { enabled?: boolean } = {}) =>
- useQuery({ enabled, ...queryKeys.user.summary });
+ useQuery({ ...queryKeys.user.summary, enabled });
export default useUserSummary;