diff --git a/frontend/.gitignore b/frontend/.gitignore index f0abd802..348bb0b4 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -11,6 +11,7 @@ node_modules dist dist-ssr *.local +.env* # Editor directories and files .vscode/* diff --git a/frontend/apps/client/src/features/login/api/mutations.ts b/frontend/apps/client/src/features/login/api/mutations.ts index 1ed1f5b2..d989f27f 100644 --- a/frontend/apps/client/src/features/login/api/mutations.ts +++ b/frontend/apps/client/src/features/login/api/mutations.ts @@ -1,5 +1,5 @@ -import { useMutation } from '@tanstack/react-query'; -import { useNavigate } from '@tanstack/react-router'; +import { createMutation } from '@endolphin/core/utils'; +import { redirect } from '@tanstack/react-router'; import { accessTokenService } from '@utils/auth/accessTokenService'; import type { JWTRequest } from '../model'; @@ -9,23 +9,21 @@ interface JWTMutationProps extends JWTRequest { lastPath: string | null; } -export const useJWTMutation = () => { - const navigate = useNavigate(); - - const { mutate } = useMutation({ +export const jwtMutation = () => { + const { mutateAsync } = createMutation({ mutationFn: ({ code }: JWTMutationProps) => loginApi.getJWT(code), onSuccess: (response, { lastPath }) => { accessTokenService.setAccessToken(response); - navigate({ + throw redirect({ to: lastPath || '/home', }); }, onError: () => { - navigate({ + throw redirect({ to: '/login', }); }, }); - return { loginMutate: mutate }; + return { loginMutate: mutateAsync }; }; \ No newline at end of file diff --git a/frontend/apps/client/src/main.tsx b/frontend/apps/client/src/main.tsx index c54eb488..790de90e 100644 --- a/frontend/apps/client/src/main.tsx +++ b/frontend/apps/client/src/main.tsx @@ -1,3 +1,4 @@ +import { setDefaultMutationErrorHandler } from '@endolphin/core/utils'; import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { createRouter, RouterProvider } from '@tanstack/react-router'; import { StrictMode } from 'react'; @@ -41,6 +42,8 @@ const router = createRouter({ }, }); +setDefaultMutationErrorHandler(handleError); + declare module '@tanstack/react-router' { interface Register { router: typeof router; diff --git a/frontend/apps/client/src/routes/oauth.redirect/calendar/index.tsx b/frontend/apps/client/src/routes/oauth.redirect/calendar/index.tsx index cd68906c..f1016f8c 100644 --- a/frontend/apps/client/src/routes/oauth.redirect/calendar/index.tsx +++ b/frontend/apps/client/src/routes/oauth.redirect/calendar/index.tsx @@ -1,23 +1,10 @@ -import { useQueryClient } from '@tanstack/react-query'; -import { createFileRoute, useNavigate } from '@tanstack/react-router'; -import { useEffect } from 'react'; +import { createFileRoute, redirect } from '@tanstack/react-router'; import { calendarKeys } from '@/features/my-calendar/api/keys'; -const Redirect = () => { - const queryClient = useQueryClient(); - const navigate = useNavigate(); - - useEffect(() => { - (async () => { - await queryClient.invalidateQueries({ queryKey: calendarKeys.all }); - navigate({ to: '/my-calendar' }); - })(); - }, [queryClient, navigate]); - - return null; -}; - export const Route = createFileRoute('/oauth/redirect/calendar/')({ - component: Redirect, + beforeLoad: async ({ context }) => { + await context.queryClient.invalidateQueries({ queryKey: calendarKeys.all }); + throw redirect({ to: '/my-calendar' }); + }, }); \ No newline at end of file diff --git a/frontend/apps/client/src/routes/oauth.redirect/login/index.tsx b/frontend/apps/client/src/routes/oauth.redirect/login/index.tsx index 8e3a91b9..47a6b43e 100644 --- a/frontend/apps/client/src/routes/oauth.redirect/login/index.tsx +++ b/frontend/apps/client/src/routes/oauth.redirect/login/index.tsx @@ -1,24 +1,18 @@ -import { createFileRoute, useSearch } from '@tanstack/react-router'; -import { useEffect } from 'react'; +import { createFileRoute } from '@tanstack/react-router'; -import { useJWTMutation } from '@/features/login/api/mutations'; +import { jwtMutation } from '@/features/login/api/mutations'; import { getLastRoutePath } from '@/utils/route'; -const Redirect = () => { - const { loginMutate } = useJWTMutation(); - const lastPath = getLastRoutePath(); - const params: { code: string } = useSearch({ from: '/oauth/redirect/login/' }); - const { code } = params; - - useEffect(() => { - if (code) { - loginMutate({ code, lastPath }); - } - }, [code, loginMutate, lastPath]); - - return null; +type SearchWithCode = { + code?: string; }; export const Route = createFileRoute('/oauth/redirect/login/')({ - component: Redirect, + beforeLoad: async ({ search }: { search: SearchWithCode }) => { + const lastPath = getLastRoutePath(); + const { loginMutate } = jwtMutation(); + if (search.code) { + await loginMutate({ code: search.code, lastPath }); + } + }, }); \ No newline at end of file diff --git a/frontend/packages/core/src/utils/index.ts b/frontend/packages/core/src/utils/index.ts index ed1f3473..665a8368 100644 --- a/frontend/packages/core/src/utils/index.ts +++ b/frontend/packages/core/src/utils/index.ts @@ -2,4 +2,5 @@ export { default as clsx } from './clsx'; export * from './common'; export * from './context'; export * from './date'; -export * from './jsx'; \ No newline at end of file +export * from './jsx'; +export * from './network'; \ No newline at end of file diff --git a/frontend/packages/core/src/utils/network/createMutation.ts b/frontend/packages/core/src/utils/network/createMutation.ts new file mode 100644 index 00000000..46033be1 --- /dev/null +++ b/frontend/packages/core/src/utils/network/createMutation.ts @@ -0,0 +1,54 @@ +type MutationFn = (args: TArgs) => Promise; + +interface RedirectMutationOptions { + mutationFn: MutationFn; + onSuccess?: (response: TRes, args: TArgs) => unknown; + onError?: (error: unknown, args: TArgs) => unknown; +} + +type Mutation = { + mutate: (args: TArgs) => void; + mutateAsync: (args: TArgs) => Promise; +}; + +let globalHandleError: (error: unknown) => unknown; + +const isRedirectError = (error: unknown): boolean => ( + typeof error === 'object' && error !== null + && 'isRedirect' in error && error.isRedirect === true +); + +export const setDefaultMutationErrorHandler = (fn: (error: unknown) => void) => { + globalHandleError = fn; +}; + +/** + * 리액트 컴포넌트 외부에서 사용할 수 있는 뮤테이션 함수 생성기. + */ +export const createMutation = ( + options: RedirectMutationOptions, +): Mutation => { + + const mutateAsync = async (args: TArgs) => { + try { + const response = await options.mutationFn(args); + options.onSuccess?.(response, args); + } catch (error) { + if (isRedirectError(error)) throw error; + globalHandleError?.(error); + options.onError?.(error, args); + throw error; + } + }; + + const mutate = (args: TArgs) => { + options.mutationFn(args) + .then(res => options.onSuccess?.(res, args)) + .catch(err => { + globalHandleError?.(err); + options.onError?.(err, args); + }); + }; + + return { mutate, mutateAsync }; +}; diff --git a/frontend/packages/core/src/utils/network/index.ts b/frontend/packages/core/src/utils/network/index.ts new file mode 100644 index 00000000..f8dac7fb --- /dev/null +++ b/frontend/packages/core/src/utils/network/index.ts @@ -0,0 +1 @@ +export * from './createMutation'; \ No newline at end of file