Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
082b0b4
๐Ÿ› fix: debounce err ์ˆ˜์ •
minkyung5x5 Nov 14, 2024
c63c880
๐Ÿ› fix: debounce err ์ˆ˜์ •
minkyung5x5 Nov 14, 2024
f710f78
โœจ Feat: refreshToken ์ถ”๊ฐ€, logoutAPI
minkyung5x5 Nov 14, 2024
51f3f04
โ™ป๏ธ refactor: ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ ๋ณ€๊ฒฝ
minkyung5x5 Nov 14, 2024
808053b
๐Ÿ› fix: ์Šคํ† ๋ฆฌ๋ถ ํ—ค๋”์— refreshToken ์ถ”๊ฐ€
minkyung5x5 Nov 14, 2024
8dd04ff
Merge branch 'develop' into Feat/118/auth-api
minkyung5x5 Nov 15, 2024
d242fa8
โœจ Feat: ๋กœ๊ทธ์ธ, ํšŒ์›๊ฐ€์ž…์‹œ redirect ์ถ”๊ฐ€
minkyung5x5 Nov 15, 2024
f29d1c2
Merge branch 'develop' into Feat/118/auth-api
minkyung5x5 Nov 15, 2024
2fa147d
โœจ Feat: ๋ฆฌํ”„๋ ˆ์‹œํ† ํฐ ์ฟ ํ‚ค๋กœ ์ „๋‹ฌ๋ฐ›๋Š” ๊ฒƒ์œผ๋กœ ์ˆ˜์ •
minkyung5x5 Nov 18, 2024
548464a
๐Ÿ› fix: ํ—ค๋” ์Šคํ† ๋ฆฌ๋ถ์—์„œ refreshtoken ์‚ญ์ œ
minkyung5x5 Nov 18, 2024
f761eb1
โœจ Feat: reissue token ์ถ”๊ฐ€
minkyung5x5 Nov 18, 2024
c581727
๐Ÿ› fix: useGetUserQuery ์ˆ˜์ •
minkyung5x5 Nov 18, 2024
32a3ea8
๐Ÿ› fix: reissue ์—๋Ÿฌ ์ˆ˜์ •, โ™ป๏ธ refactor: ์ƒํƒœ๊ด€๋ฆฌ ๋ฆฌํŒฉํ† ๋ง,
minkyung5x5 Nov 19, 2024
b58a460
โ™ป๏ธ refactor: prettier ์ ์šฉ
minkyung5x5 Nov 19, 2024
acb0d3b
๐Ÿ› fix: suspense err ์ˆ˜์ •
minkyung5x5 Nov 19, 2024
35898d7
Merge branch 'develop' into Feat/118/auth-api
minkyung5x5 Nov 19, 2024
1ca2b4c
๐Ÿ› fix: isAuth ํ˜ธ์ถœ์œ„์น˜ ๋ณ€๊ฒฝ ๋ฐ˜์˜
minkyung5x5 Nov 19, 2024
986cc57
๐Ÿ› fix: user ํ˜ธ์ถœ ์œ„์น˜ ๋ณ€๊ฒฝ
minkyung5x5 Nov 19, 2024
8205092
๐Ÿ› fix: header storybook err ์ˆ˜์ •
minkyung5x5 Nov 19, 2024
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
10 changes: 10 additions & 0 deletions src/_apis/auth/logout-apis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { fetchApi } from '@/src/utils/api';

export function logout(): Promise<void> {
return fetchApi('/auths/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
}
18 changes: 18 additions & 0 deletions src/_apis/auth/reissue-apis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { fetchApi } from '@/src/utils/api';

export async function reissue(): Promise<{ token: string | null }> {
return fetchApi<{ headers: Headers }>(
'/auths/reissue',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
5000,
true,
).then((response) => {
const token = response.headers.get('Authorization');
return { token };
});
}
Comment on lines +3 to +18
Copy link

Choose a reason for hiding this comment

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

๐Ÿ› ๏ธ Refactor suggestion

ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ๋กœ์ง ๊ฐœ์„  ํ•„์š”

๋‹ค์Œ ์‚ฌํ•ญ๋“ค์˜ ๊ฐœ์„ ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค:

  1. ์‘๋‹ต ํƒ€์ž… ์•ˆ์ „์„ฑ์ด ๋ณด์žฅ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค
  2. ํ† ํฐ ํ˜•์‹ ๊ฒ€์ฆ์ด ์—†์Šต๋‹ˆ๋‹ค
  3. ํƒ€์ž„์•„์›ƒ ๊ฐ’์ด ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค

๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ฐœ์„ ํ•˜๋Š” ๊ฒƒ์„ ์ œ์•ˆ๋“œ๋ฆฝ๋‹ˆ๋‹ค:

+interface ReissueResponse {
+  headers: Headers;
+}
+
+const REISSUE_TIMEOUT = 5000;
+
-export async function reissue(): Promise<{ token: string | null }> {
+export async function reissue(): Promise<{ token: string | null }> {
   return fetchApi<{ headers: Headers }>(
     '/auths/reissue',
     {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',
       },
     },
-    5000,
+    REISSUE_TIMEOUT,
     true,
   ).then((response) => {
     const token = response.headers.get('Authorization');
+    if (token && !token.startsWith('Bearer ')) {
+      throw new Error('Invalid token format');
+    }
     return { token };
   });
 }

์ถ”๊ฐ€๋กœ ๊ณ ๋ คํ•ด์•ผ ํ•  ์‚ฌํ•ญ:

  1. ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์‹คํŒจ ์‹œ์˜ ์žฌ์‹œ๋„ ๋กœ์ง
  2. ๋กœ๊น… ์ถ”๊ฐ€
  3. ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ƒ์„ธํ™”
๐Ÿ“ 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
export async function reissue(): Promise<{ token: string | null }> {
return fetchApi<{ headers: Headers }>(
'/auths/reissue',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
5000,
true,
).then((response) => {
const token = response.headers.get('Authorization');
return { token };
});
}
interface ReissueResponse {
headers: Headers;
}
const REISSUE_TIMEOUT = 5000;
export async function reissue(): Promise<{ token: string | null }> {
return fetchApi<{ headers: Headers }>(
'/auths/reissue',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
REISSUE_TIMEOUT,
true,
).then((response) => {
const token = response.headers.get('Authorization');
if (token && !token.startsWith('Bearer ')) {
throw new Error('Invalid token format');
}
return { token };
});
}

2 changes: 1 addition & 1 deletion src/_queries/auth/login-queries.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useMutation } from '@tanstack/react-query';
import { login } from '@/src/_apis/auth/login-apis';
import { useHandleAuthSuccess } from '@/src/hooks/use-handle-auth-success';
import { ApiError } from '@/src/utils/api';
import { LoginRequest, LoginResponse } from '@/src/types/auth';
import { useHandleAuthSuccess } from './use-handle-auth-success';

export function usePostLoginQuery() {
const handleAuthSuccess = useHandleAuthSuccess();
Expand Down
9 changes: 9 additions & 0 deletions src/_queries/auth/logout-queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useMutation } from '@tanstack/react-query';
import { logout } from '@/src/_apis/auth/logout-apis';
import { ApiError } from '@/src/utils/api';

export function usePostLogoutQuery() {
return useMutation<void, ApiError>({
mutationFn: logout,
});
}
15 changes: 15 additions & 0 deletions src/_queries/auth/reissue-queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useMutation } from '@tanstack/react-query';
import { reissue } from '@/src/_apis/auth/reissue-apis';
import { useHandleAuthSuccess } from '@/src/hooks/use-handle-auth-success';
import { ApiError } from '@/src/utils/api';

export function usePostReissueQuery() {
const handleAuthSuccess = useHandleAuthSuccess();

return useMutation<{ token: string | null }, ApiError>({
mutationFn: reissue,
onSuccess: async (response) => {
await handleAuthSuccess(response.token);
},
});
Comment on lines +9 to +14
Copy link

Choose a reason for hiding this comment

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

๐Ÿ› ๏ธ Refactor suggestion

์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋กœ์ง ์ถ”๊ฐ€๋ฅผ ๊ณ ๋ คํ•ด์ฃผ์„ธ์š”.

ํ˜„์žฌ ์„ฑ๊ณต ์ผ€์ด์Šค๋งŒ ์ฒ˜๋ฆฌ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์‹คํŒจ ์‹œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ ์ ˆํ•œ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•˜๊ณ  ํ•„์š”ํ•œ ํ›„์† ์กฐ์น˜๋ฅผ ์ทจํ•˜๋„๋ก onError ํ•ธ๋“ค๋Ÿฌ ์ถ”๊ฐ€๋ฅผ ๊ถŒ์žฅ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์ด ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณด์„ธ์š”:

 return useMutation<{ token: string | null }, ApiError>({
   mutationFn: reissue,
   onSuccess: async (response) => {
     await handleAuthSuccess(response.token);
   },
+  onError: (error) => {
+    // ์—๋Ÿฌ ์ƒํ™ฉ์— ๋”ฐ๋ฅธ ์ฒ˜๋ฆฌ
+    if (error.status === 401) {
+      // ์ธ์ฆ ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌ
+    } else {
+      // ๊ธฐํƒ€ ์—๋Ÿฌ ์ฒ˜๋ฆฌ
+    }
+  },
 });

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

}
2 changes: 1 addition & 1 deletion src/_queries/auth/signup-queries.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useMutation } from '@tanstack/react-query';
import { signup } from '@/src/_apis/auth/signup-apis';
import { useHandleAuthSuccess } from '@/src/hooks/use-handle-auth-success';
import { ApiError } from '@/src/utils/api';
import { SignupRequest, SignupResponse } from '@/src/types/auth';
import { useHandleAuthSuccess } from './use-handle-auth-success';

export function usePostSignupQuery() {
const handleAuthSuccess = useHandleAuthSuccess();
Expand Down
11 changes: 0 additions & 11 deletions src/_queries/auth/user-apis.ts

This file was deleted.

12 changes: 12 additions & 0 deletions src/_queries/auth/user-queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useQuery } from '@tanstack/react-query';
import { getUser } from '@/src/_apis/auth/user-apis';
import { authStore } from '@/src/store/use-auth-store';

export function useUser() {
const { token } = authStore.getState();
return useQuery({
queryKey: ['user'],
queryFn: getUser,
enabled: !!token,
});
}
Comment on lines +5 to +12
Copy link

Choose a reason for hiding this comment

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

๐Ÿ› ๏ธ Refactor suggestion

ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๊ฐ•ํ™” ํ•„์š”

ํ˜„์žฌ ๊ตฌํ˜„์€ ํ† ํฐ์˜ ์กด์žฌ ์—ฌ๋ถ€๋งŒ ํ™•์ธํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋ณด์•ˆ ๊ฐ•ํ™”๋ฅผ ์œ„ํ•ด ๋‹ค์Œ ์‚ฌํ•ญ๋“ค์„ ๊ณ ๋ คํ•ด์ฃผ์„ธ์š”:

  1. ํ† ํฐ ๋งŒ๋ฃŒ ์—ฌ๋ถ€ ํ™•์ธ
  2. ํ† ํฐ ํ˜•์‹ ๊ฒ€์ฆ
  3. ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋กœ์ง ์ถ”๊ฐ€

๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ฐœ์„ ํ•  ๊ฒƒ์„ ์ œ์•ˆ๋“œ๋ฆฝ๋‹ˆ๋‹ค:

 export function useUser() {
   const { token } = authStore.getState();
+  const isValidToken = token && !isTokenExpired(token);
   return useQuery({
     queryKey: ['user'],
     queryFn: getUser,
-    enabled: !!token,
+    enabled: isValidToken,
+    retry: false,
+    onError: (error) => {
+      if (error.response?.status === 401) {
+        authStore.getState().logout();
+      }
+    },
   });
 }

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

5 changes: 4 additions & 1 deletion src/app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Suspense } from 'react';
import type { Metadata } from 'next';
import Image from 'next/image';
import '@mantine/core/styles.css';
Expand Down Expand Up @@ -30,7 +31,9 @@ export default function AuthLayout({
<div className="mt-4 text-center text-sm font-semibold lg:text-base">
ํ•จ๊ป˜ํ•  ์‚ฌ๋žŒ์ด์—†๋‚˜์š”? ์ง€๊ธˆ ํฌ๋ฃจ์— ์ฐธ์—ฌํ•ด๋ณด์„ธ์š”
</div>
<div className="mt-6 w-full md:mt-12 md:w-2/3 lg:w-1/2">{children}</div>
<div className="mt-6 w-full md:mt-12 md:w-2/3 lg:w-1/2">
<Suspense>{children}</Suspense>
</div>
Comment on lines +34 to +36
Copy link

Choose a reason for hiding this comment

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

๐Ÿ› ๏ธ Refactor suggestion

Suspense์— fallback UI ์ถ”๊ฐ€ ํ•„์š”

๋กœ๋”ฉ ์ƒํƒœ๋ฅผ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•ด fallback prop์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •ํ•˜๋Š” ๊ฒƒ์„ ์ œ์•ˆ๋“œ๋ฆฝ๋‹ˆ๋‹ค:

  <div className="mt-6 w-full md:mt-12 md:w-2/3 lg:w-1/2">
-   <Suspense>{children}</Suspense>
+   <Suspense fallback={
+     <div className="flex justify-center items-center h-full">
+       <div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary"></div>
+     </div>
+   }>
+     {children}
+   </Suspense>
  </div>
๐Ÿ“ 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
<div className="mt-6 w-full md:mt-12 md:w-2/3 lg:w-1/2">
<Suspense>{children}</Suspense>
</div>
<div className="mt-6 w-full md:mt-12 md:w-2/3 lg:w-1/2">
<Suspense fallback={
<div className="flex justify-center items-center h-full">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary"></div>
</div>
}>
{children}
</Suspense>
</div>

</div>
</div>
</div>
Expand Down
38 changes: 27 additions & 11 deletions src/app/(auth)/login/_component/login-form.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { UseFormReturn } from 'react-hook-form';
import { Button } from '@mantine/core';
import { useDebouncedCallback } from '@mantine/hooks';
import { useEffect } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
import { useDebouncedValue } from '@mantine/hooks';
import Button from '@/src/components/common/input/button';
import PasswordInput from '@/src/components/common/input/password-input';
import TextInput from '@/src/components/common/input/text-input';

Expand All @@ -18,15 +19,29 @@ export default function LoginForm({ onSubmit, formMethods }: LoginFormProps) {
register,
handleSubmit,
trigger,
control,
formState: { errors, isValid },
} = formMethods;

const debouncedTrigger = useDebouncedCallback(async (field: keyof LoginFormValues) => {
await trigger(field);
}, 1000);
const email = useWatch({ control, name: 'email' });
const password = useWatch({ control, name: 'password' });

const [debouncedEmail, cancelDebouncedEmail] = useDebouncedValue(email, 1000);
const [debouncedPassword, cancelDebouncedPassword] = useDebouncedValue(password, 1000);

useEffect(() => {
if (debouncedEmail) trigger('email');
if (debouncedPassword) trigger('password');
}, [debouncedEmail, debouncedPassword, trigger]);

const handleFormSubmit = handleSubmit(async (data) => {
onSubmit(data);
cancelDebouncedEmail();
cancelDebouncedPassword();
});
Comment on lines +37 to +41
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue

ํผ ์ œ์ถœ ํ•ธ๋“ค๋Ÿฌ ๋กœ์ง ๊ฐœ์„  ํ•„์š”

๋””๋ฐ”์šด์Šค ์ทจ์†Œ ๋กœ์ง์ด ํผ ์ œ์ถœ ํ›„์— ์‹คํ–‰๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์ž ์žฌ์ ์œผ๋กœ ๊ฒฝ์Ÿ ์ƒํƒœ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •์„ ์ œ์•ˆํ•ฉ๋‹ˆ๋‹ค:

  const handleFormSubmit = handleSubmit(async (data) => {
+   cancelDebouncedEmail();
+   cancelDebouncedPassword();
    onSubmit(data);
-   cancelDebouncedEmail();
-   cancelDebouncedPassword();
  });
๐Ÿ“ 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 handleFormSubmit = handleSubmit(async (data) => {
onSubmit(data);
cancelDebouncedEmail();
cancelDebouncedPassword();
});
const handleFormSubmit = handleSubmit(async (data) => {
cancelDebouncedEmail();
cancelDebouncedPassword();
onSubmit(data);
});


return (
<form onSubmit={handleSubmit(onSubmit)}>
<form onSubmit={handleFormSubmit}>
<TextInput
label="์•„์ด๋””"
placeholder="์ด๋ฉ”์ผ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"
Expand All @@ -38,7 +53,6 @@ export default function LoginForm({ onSubmit, formMethods }: LoginFormProps) {
message: '์ด๋ฉ”์ผ ํ˜•์‹์œผ๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”',
},
onBlur: () => trigger('email'),
onChange: async () => debouncedTrigger('email'),
}),
}}
error={errors.email?.message}
Expand All @@ -56,7 +70,6 @@ export default function LoginForm({ onSubmit, formMethods }: LoginFormProps) {
message: '๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ 8์ž ์ด์ƒ์ด ๋˜๋„๋ก ํ•ด์ฃผ์„ธ์š”.',
},
onBlur: () => trigger('password'),
onChange: async () => debouncedTrigger('password'),
}),
}}
error={errors.password?.message}
Expand All @@ -65,8 +78,11 @@ export default function LoginForm({ onSubmit, formMethods }: LoginFormProps) {
/>

<div className="mt-10">
{/* TODO: Button ๋ฐ”๊พธ๊ธฐ */}
<Button disabled={!isValid} type="submit" fullWidth>
<Button
disabled={!isValid}
type="submit"
className={`w-full font-semibold ${isValid ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-300'}`}
>
๋กœ๊ทธ์ธ
</Button>
</div>
Expand Down
18 changes: 14 additions & 4 deletions src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
'use client';

import React from 'react';
import React, { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useRouter, useSearchParams } from 'next/navigation';
import { usePostLoginQuery } from '@/src/_queries/auth/login-queries';
import LoginForm, { LoginFormValues } from './_component/login-form';

export default function LoginPage() {
const [redirect, setRedirect] = useState('/');
const searchParams = useSearchParams();
const router = useRouter();
const formMethods = useForm<LoginFormValues>();
const { setError } = formMethods;
const { mutate: postLogin } = usePostLoginQuery();

useEffect(() => {
const redirectParam = searchParams.get('redirect');
if (redirectParam) setRedirect(redirectParam);
}, [searchParams]);

const handleSubmit = async (data: LoginFormValues) => {
postLogin(data, {
onSuccess: () => {
router.push('/');
router.push(redirect);
},
onError: (error) => {
if (error.status === 401) {
Expand All @@ -38,7 +45,10 @@ export default function LoginPage() {
<LoginForm formMethods={formMethods} onSubmit={handleSubmit} />
<div className="mt-6 flex justify-center space-x-1 text-sm font-medium">
<div>ํฌ๋ฃจ๊ฐ€ ์ฒ˜์Œ์ด์‹ ๊ฐ€์š”?</div>
<Link href="/signup" className="text-blue-500 underline">
<Link
href={redirect !== '/' ? `/signup?redirect=${redirect}` : '/signup'}
className="text-blue-500 underline"
>
Comment on lines +48 to +51
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue

๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URL ์ธ์ฝ”๋”ฉ ํ•„์š”

๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋Œ€ํ•œ URL ์ธ์ฝ”๋”ฉ์ด ๋ˆ„๋ฝ๋˜์–ด ์žˆ์–ด ๋ณด์•ˆ ์ทจ์•ฝ์ ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅ๋“œ๋ฆฝ๋‹ˆ๋‹ค:

 <Link
-  href={redirect !== '/' ? `/signup?redirect=${redirect}` : '/signup'}
+  href={redirect !== '/' ? `/signup?redirect=${encodeURIComponent(redirect)}` : '/signup'}
   className="text-blue-500 underline"
 >
๐Ÿ“ 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
<Link
href={redirect !== '/' ? `/signup?redirect=${redirect}` : '/signup'}
className="text-blue-500 underline"
>
<Link
href={redirect !== '/' ? `/signup?redirect=${encodeURIComponent(redirect)}` : '/signup'}
className="text-blue-500 underline"
>

ํšŒ์›๊ฐ€์ž…
</Link>
</div>
Expand Down
59 changes: 43 additions & 16 deletions src/app/(auth)/signup/_component/signup-form.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import { UseFormReturn } from 'react-hook-form';
import { Button } from '@mantine/core';
import { useDebouncedCallback } from '@mantine/hooks';
import { UseFormReturn, useWatch } from 'react-hook-form';
import { useDebouncedValue } from '@mantine/hooks';
import Button from '@/src/components/common/input/button';
import PasswordInput from '@/src/components/common/input/password-input';
import TextInput from '@/src/components/common/input/text-input';

Expand All @@ -22,32 +22,59 @@ export default function SignupForm({ formMethods, onSubmit }: SignupFormProps) {
register,
handleSubmit,
trigger,
control,
formState: { errors, isValid },
watch,
} = formMethods;

const debouncedTrigger = useDebouncedCallback(async (field: keyof SignupFormValues) => {
await trigger(field);
}, 1000);
const [nickname, email, password, confirmPassword] = useWatch({
control,
name: ['nickname', 'email', 'password', 'confirmPassword'],
});

const password = watch('password');
const [debouncedNickname, cancelDebouncedNickname] = useDebouncedValue(nickname, 1000);
const [debouncedEmail, cancelDebouncedEmail] = useDebouncedValue(email, 1000);
const [debouncedPassword, cancelDebouncedPassword] = useDebouncedValue(password, 1000);
const [debouncedConfirmPassword, cancelDebouncedConfirmPassword] = useDebouncedValue(
confirmPassword,
1000,
);

useEffect(() => {
if (debouncedNickname) trigger('nickname');
}, [nickname, trigger]);

useEffect(() => {
if (debouncedEmail) trigger('email');
}, [email, trigger]);

useEffect(() => {
if (password) {
if (debouncedPassword) {
trigger('password');
trigger('confirmPassword');
}
}, [password, trigger]);

useEffect(() => {
if (debouncedConfirmPassword) trigger('confirmPassword');
}, [confirmPassword, trigger]);
Comment on lines +42 to +59
Copy link

Choose a reason for hiding this comment

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

๐Ÿ› ๏ธ Refactor suggestion

์œ ํšจ์„ฑ ๊ฒ€์ฆ ๋กœ์ง์ด ์ค‘๋ณต๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฐ ํ•„๋“œ๋งˆ๋‹ค ๋น„์Šทํ•œ useEffect ํ›…์ด ๋ฐ˜๋ณต๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์ฝ”๋“œ์˜ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ์ €ํ•˜์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•˜๋‚˜์˜ useEffect๋กœ ํ†ตํ•ฉํ•˜๋Š” ๊ฒƒ์„ ์ถ”์ฒœ๋“œ๋ฆฝ๋‹ˆ๋‹ค:

useEffect(() => {
  const fields = {
    nickname: debouncedNickname,
    email: debouncedEmail,
    password: debouncedPassword,
    confirmPassword: debouncedConfirmPassword,
  };

  Object.entries(fields).forEach(([field, value]) => {
    if (value) {
      trigger(field as keyof SignupFormValues);
      if (field === 'password') {
        trigger('confirmPassword');
      }
    }
  });
}, [debouncedNickname, debouncedEmail, debouncedPassword, debouncedConfirmPassword, trigger]);


const handleFormSubmit = handleSubmit(async (data) => {
onSubmit(data);
cancelDebouncedNickname();
cancelDebouncedEmail();
cancelDebouncedPassword();
cancelDebouncedConfirmPassword();
});

return (
<form onSubmit={handleSubmit(onSubmit)}>
<form onSubmit={handleFormSubmit}>
<TextInput
label="๋‹‰๋„ค์ž„"
placeholder="๋‹‰๋„ค์ž„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"
register={{
...register('nickname', {
required: '๋‹‰๋„ค์ž„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”',
onBlur: () => trigger('nickname'),
onChange: async () => debouncedTrigger('nickname'),
}),
}}
error={errors.nickname?.message}
Expand All @@ -65,7 +92,6 @@ export default function SignupForm({ formMethods, onSubmit }: SignupFormProps) {
message: '์ด๋ฉ”์ผ ํ˜•์‹์œผ๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”',
},
onBlur: () => trigger('email'),
onChange: async () => debouncedTrigger('email'),
}),
}}
error={errors.email?.message}
Expand All @@ -84,7 +110,6 @@ export default function SignupForm({ formMethods, onSubmit }: SignupFormProps) {
message: '๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ 8์ž ์ด์ƒ์ด ๋˜๋„๋ก ํ•ด์ฃผ์„ธ์š”.',
},
onBlur: () => trigger('password'),
onChange: async () => debouncedTrigger('password'),
}),
}}
error={errors.password?.message}
Expand All @@ -100,7 +125,6 @@ export default function SignupForm({ formMethods, onSubmit }: SignupFormProps) {
required: '๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋‹ค์‹œ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”',
validate: (value) => value === password || '๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.',
onBlur: () => trigger('confirmPassword'),
onChange: async () => debouncedTrigger('confirmPassword'),
}),
}}
error={errors.confirmPassword?.message}
Expand All @@ -109,8 +133,11 @@ export default function SignupForm({ formMethods, onSubmit }: SignupFormProps) {
/>

<div className="mt-10">
{/* TODO: button ๋ฐ”๊พธ๊ธฐ */}
<Button disabled={!isValid} type="submit" fullWidth>
<Button
disabled={!isValid}
type="submit"
className={`w-full font-semibold ${isValid ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-300'}`}
>
ํšŒ์›๊ฐ€์ž…
</Button>
</div>
Expand Down
Loading
Loading