Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ node_modules
dist
dist-ssr
*.local
.env*

# Editor directories and files
.vscode/*
Expand Down
16 changes: 7 additions & 9 deletions frontend/apps/client/src/features/login/api/mutations.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 };
};
3 changes: 3 additions & 0 deletions frontend/apps/client/src/main.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -41,6 +42,8 @@ const router = createRouter({
},
});

setDefaultMutationErrorHandler(handleError);

declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
Expand Down
Original file line number Diff line number Diff line change
@@ -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' });
},
});
28 changes: 11 additions & 17 deletions frontend/apps/client/src/routes/oauth.redirect/login/index.tsx
Original file line number Diff line number Diff line change
@@ -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 });
}
},
Comment on lines +11 to +17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling and validation for the beforeLoad lifecycle.

The beforeLoad function lacks proper error handling and validation:

  1. Missing code validation: The function should validate that search.code exists and is a valid string before proceeding with the mutation.
  2. Unhandled mutation errors: If loginMutate throws an error that isn't a redirect, it will cause the route loading to fail without proper user feedback.
  3. Type safety concern: The SearchWithCode type allows code to be undefined, but the logic assumes it exists.
export const Route = createFileRoute('/oauth/redirect/login/')({
- beforeLoad: async ({ search }: { search: SearchWithCode }) => {
+ beforeLoad: async ({ search }: { search: SearchWithCode }) => {
+   if (!search.code) {
+     throw redirect({ to: '/login' });
+   }
+   
    const lastPath = getLastRoutePath();
    const { loginMutate } = jwtMutation();
-   if (search.code) {
-     await loginMutate({ code: search.code, lastPath });
-   }
+   
+   try {
+     await loginMutate({ code: search.code, lastPath });
+   } catch (error) {
+     // Let redirect errors bubble up, handle others
+     if (error && typeof error === 'object' && 'isRedirect' in error) {
+       throw error;
+     }
+     throw redirect({ to: '/login' });
+   }
  },
});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
beforeLoad: async ({ search }: { search: SearchWithCode }) => {
const lastPath = getLastRoutePath();
const { loginMutate } = jwtMutation();
if (search.code) {
await loginMutate({ code: search.code, lastPath });
}
},
export const Route = createFileRoute('/oauth/redirect/login/')({
beforeLoad: async ({ search }: { search: SearchWithCode }) => {
// Validate that we actually have a code, otherwise redirect to login
if (!search.code) {
throw redirect({ to: '/login' });
}
const lastPath = getLastRoutePath();
const { loginMutate } = jwtMutation();
try {
await loginMutate({ code: search.code, lastPath });
} catch (error) {
// If it’s a redirect-error, let it bubble up; otherwise go back to login
if (error && typeof error === 'object' && 'isRedirect' in error) {
throw error;
}
throw redirect({ to: '/login' });
}
},
});
🤖 Prompt for AI Agents
In frontend/apps/client/src/routes/oauth.redirect/login/index.tsx around lines
11 to 17, the beforeLoad function needs improved error handling and validation.
First, add a check to ensure search.code is a defined, valid string before
calling loginMutate. Then, wrap the loginMutate call in a try-catch block to
handle any errors; if the error is not a redirect, catch it and provide
appropriate user feedback or logging to prevent route loading failure. Also,
adjust the type handling to safely account for the possibility that search.code
may be undefined.

});
3 changes: 2 additions & 1 deletion frontend/packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export { default as clsx } from './clsx';
export * from './common';
export * from './context';
export * from './date';
export * from './jsx';
export * from './jsx';
export * from './network';
54 changes: 54 additions & 0 deletions frontend/packages/core/src/utils/network/createMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
type MutationFn<TArgs, TRes> = (args: TArgs) => Promise<TRes>;

interface RedirectMutationOptions<TArgs, TRes> {
mutationFn: MutationFn<TArgs, TRes>;
onSuccess?: (response: TRes, args: TArgs) => unknown;
onError?: (error: unknown, args: TArgs) => unknown;
}

type Mutation<TArgs> = {
mutate: (args: TArgs) => void;
mutateAsync: (args: TArgs) => Promise<void>;
};

let globalHandleError: (error: unknown) => unknown;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Initialize the global error handler to prevent runtime errors.

The global error handler is declared but not initialized, which could cause issues if createMutation is used before setDefaultMutationErrorHandler is called:

-let globalHandleError: (error: unknown) => unknown;
+let globalHandleError: (error: unknown) => unknown = () => {
+  // Default no-op handler to prevent runtime errors
+};

Or alternatively, add proper null checking:

-      globalHandleError?.(error);
+      if (globalHandleError) {
+        globalHandleError(error);
+      }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In frontend/packages/core/src/utils/network/createMutation.ts at line 14, the
global error handler variable is declared but not initialized, which can cause
runtime errors if used before being set. Initialize globalHandleError with a
default no-op function or a function that safely handles errors to prevent
undefined behavior. Alternatively, add null or undefined checks before invoking
globalHandleError to ensure safe usage.


const isRedirectError = (error: unknown): boolean => (
typeof error === 'object' && error !== null
&& 'isRedirect' in error && error.isRedirect === true
);
Comment on lines +16 to +19
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve type safety of the redirect error detection.

The isRedirectError function uses loose type checking that could lead to false positives:

-const isRedirectError = (error: unknown): boolean => (
-  typeof error === 'object' && error !== null 
-    && 'isRedirect' in error && error.isRedirect === true
-);
+const isRedirectError = (error: unknown): error is { isRedirect: true } => (
+  typeof error === 'object' && 
+  error !== null && 
+  'isRedirect' in error && 
+  (error as { isRedirect: unknown }).isRedirect === true
+);

This provides:

  1. Type guard return type: The function now acts as a proper type guard
  2. Safer property access: Explicitly casts to access the isRedirect property
  3. Better type inference: TypeScript will know the error type after the guard
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isRedirectError = (error: unknown): boolean => (
typeof error === 'object' && error !== null
&& 'isRedirect' in error && error.isRedirect === true
);
const isRedirectError = (error: unknown): error is { isRedirect: true } => (
typeof error === 'object' &&
error !== null &&
'isRedirect' in error &&
(error as { isRedirect: unknown }).isRedirect === true
);
🤖 Prompt for AI Agents
In frontend/packages/core/src/utils/network/createMutation.ts around lines 16 to
19, the isRedirectError function uses loose type checking that may cause false
positives. Refactor the function to be a proper TypeScript type guard by
specifying its return type accordingly, explicitly cast the error to a type that
includes the isRedirect property before accessing it, and ensure the function
narrows the error type for better type inference and safer property access.


export const setDefaultMutationErrorHandler = (fn: (error: unknown) => void) => {
globalHandleError = fn;
};

/**
* 리액트 컴포넌트 외부에서 사용할 수 있는 뮤테이션 함수 생성기.
*/
export const createMutation = <TArgs, TRes>(
options: RedirectMutationOptions<TArgs, TRes>,
): Mutation<TArgs> => {

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just ask;
이 부분에서는 에러를 던져주지 않아도 괜찮을까요? (궁금해서 여쭤봅니다)

});
};

return { mutate, mutateAsync };
};
1 change: 1 addition & 0 deletions frontend/packages/core/src/utils/network/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './createMutation';