-
Notifications
You must be signed in to change notification settings - Fork 1
[FEAT] 관리자 Oauth 로그인 관련 Redirect 추가 #166
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
Changes from all commits
14d3d89
91988f4
6ebe4af
131f0f7
c7cae5d
d35258e
07c7943
d34d63c
333d8f0
00f73d2
f467d26
da861db
df1ca90
f111f26
4752457
b2c13b8
7b5a27c
76fceca
6d2002c
68a4bb2
3937106
a8f092d
0f57197
a4c53cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import axios from 'axios'; | ||
| import { useEffect } from 'react'; | ||
| import { useNavigate } from 'react-router-dom'; | ||
| import { LoadingSpinner } from '@/shared/components/LoadingSpinner'; | ||
|
|
||
| export const KakaoCallback = () => { | ||
| const navigate = useNavigate(); | ||
|
|
||
| useEffect(() => { | ||
| const code = new URL(window.location.href).searchParams.get('code'); | ||
| if (!code) return; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| console.log(code); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Remove debug console.log statements. Console.log statements should be removed before merging to production. Apply this diff to remove debug logs: - console.log(code);
const fetchToken = async () => {
try {
const res = await axios.post('http://localhost:8080/api/auth/kakao/login', {
authorizationCode: code,
});
- console.log('응답res ', res);
// CASE 1) 기존 회원
...
} catch (error) {
- console.log('error:', error);
+ console.error('Kakao login failed:', error);
}Consider using a proper logger or at least Also applies to: 19-19, 34-34 🤖 Prompt for AI Agents |
||
| const fetchToken = async () => { | ||
| try { | ||
| const res = axios.post(`${import.meta.env.VITE_API_BASE_URL}/auth/kakao/login`, { | ||
| authorizationCode: code, | ||
| }); | ||
| console.log('응답res ', res); | ||
|
|
||
| // CASE 1) 기존 회원 | ||
|
|
||
| // 1-1. accessToken, refreshToken 발급 | ||
| // localStorage.setItem('accessToken', res.data.accessToken); | ||
| // localStorage.setItem('refreshToken ', res.data.refreshToken)- (수정전) | ||
| // refreshToken은 httpOnly 관리(수정후) | ||
| // ------------------------------------------------------------ | ||
| // 2-2 main 페이지 이동 | ||
| // navigate('/'); // 로그인 후 홈으로 이동 | ||
|
|
||
| // CASE 2) 기존 회원 | ||
| // 2-1. 임시 토큰 | ||
| // 2-2. navigate('/signup') | ||
| } catch (error) { | ||
| console.log('error:', error); | ||
|
Comment on lines
13
to
35
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
|
Comment on lines
+34
to
+36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| }; | ||
|
|
||
| fetchToken(); | ||
| }, [navigate]); | ||
|
|
||
| return <LoadingSpinner />; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,17 +1,25 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| import styled from '@emotion/styled'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { Button } from '@/shared/components/Button'; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const REST_API_KEY = import.meta.env.VITE_KAKAO_REST_API_KEY; | ||||||||||||||||||||||||||||||||||||||||||||||
| const REDIRECT_URI = import.meta.env.VITE_KAKAO_REDIRECT_URI; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| export const LoginButton = () => { | ||||||||||||||||||||||||||||||||||||||||||||||
| const handleKakaoLogin = () => { | ||||||||||||||||||||||||||||||||||||||||||||||
| const kakaoAuthUrl = `https://kauth.kakao.com/oauth/authorize?client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}&response_type=code`; | ||||||||||||||||||||||||||||||||||||||||||||||
| window.location.href = kakaoAuthUrl; | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+8
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❓ Verification inconclusiveVerify environment variables are defined. Ensure that Run the following script to check if environment variable examples or documentation exist: 🏁 Script executed: #!/bin/bash
# Check for .env files and documentation mentioning Kakao environment variables
fd -t f '\.env' --exec cat {} \; | rg -i 'kakao|VITE_KAKAO'
fd -t f 'README' --exec cat {} \; | rg -i 'kakao|environment|\.env'Length of output: 0 Add and document Kakao OAuth environment variables
🤖 Prompt for AI Agents
Comment on lines
+4
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate environment variables before use. The REST_API_KEY and REDIRECT_URI could be undefined if environment variables are not set. This would result in a malformed OAuth URL at runtime. Add validation and error handling: const REST_API_KEY = import.meta.env.VITE_KAKAO_REST_API_KEY;
const REDIRECT_URI = import.meta.env.VITE_KAKAO_REDIRECT_URI;
export const LoginButton = () => {
const handleKakaoLogin = () => {
+ if (!REST_API_KEY || !REDIRECT_URI) {
+ console.error('Kakao OAuth configuration missing');
+ return;
+ }
const kakaoAuthUrl = `https://kauth.kakao.com/oauth/authorize?client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}&response_type=code`;
window.location.href = kakaoAuthUrl;
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <Container> | ||||||||||||||||||||||||||||||||||||||||||||||
| <Button width='100%' type='submit'> | ||||||||||||||||||||||||||||||||||||||||||||||
| {'로그인'} | ||||||||||||||||||||||||||||||||||||||||||||||
| <Button to='/signup' width='100%' type='button'> | ||||||||||||||||||||||||||||||||||||||||||||||
| {'회원가입'} | ||||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||||
| <KakaoButtonWrapper> | ||||||||||||||||||||||||||||||||||||||||||||||
| <Button width='100%' type='submit'> | ||||||||||||||||||||||||||||||||||||||||||||||
| <Button width='100%' type='button' onClick={handleKakaoLogin}> | ||||||||||||||||||||||||||||||||||||||||||||||
| <ButtonContent> | ||||||||||||||||||||||||||||||||||||||||||||||
| <Icon src='/assets/kakao-icon.png' width={24} height={24} /> | ||||||||||||||||||||||||||||||||||||||||||||||
| <span>카카오 로그인</span> | ||||||||||||||||||||||||||||||||||||||||||||||
| {'카카오 로그인'} | ||||||||||||||||||||||||||||||||||||||||||||||
| </ButtonContent> | ||||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||||
| </KakaoButtonWrapper> | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import styled from '@emotion/styled'; | ||
| import { HeaderTitle } from './components/HeaderTitle'; | ||
| import { SignupForm } from './components/SignupForm'; | ||
|
|
||
| export const AdminSignupPage = () => { | ||
| return ( | ||
| <Layout> | ||
| <HeaderTitle /> | ||
| <SignupForm /> | ||
| </Layout> | ||
| ); | ||
| }; | ||
|
|
||
| export const Layout = styled.main(({ theme }) => ({ | ||
| minHeight: '100vh', | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| alignItems: 'center', | ||
| gap: '1.5rem', | ||
| maxWidth: '1200px', | ||
| width: '100%', | ||
| margin: '0 auto 4rem auto', | ||
| padding: '0 1.5rem', | ||
| boxSizing: 'border-box', | ||
|
|
||
| [`@media (max-width: ${theme.breakpoints.mobile})`]: { | ||
| padding: '1rem', | ||
| }, | ||
| })); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import type { SignupFormInputs } from '../type/signup'; | ||
|
|
||
| export const postSignupForm = async (formData: SignupFormInputs): Promise<SignupFormInputs> => { | ||
| const url = `${import.meta.env.VITE_API_BASE_URL}/auth/register`; | ||
| const response = await fetch(url, { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| body: JSON.stringify(formData), | ||
| }); | ||
|
|
||
| if (!response.ok) throw new Error('회원 가입 양식을 제출하지 못했습니다.'); | ||
| return await response.json(); | ||
| }; | ||
|
Comment on lines
+3
to
+15
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import styled from '@emotion/styled'; | ||
|
|
||
| export const HeaderTitle = () => { | ||
| return ( | ||
| <TextWrapper> | ||
| <Title>관리자 등록</Title> | ||
| </TextWrapper> | ||
| ); | ||
| }; | ||
| const TextWrapper = styled.div({ | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| gap: '1rem', | ||
| padding: '2rem 0 1rem 0', | ||
| }); | ||
|
|
||
| const Title = styled.h1(({ theme }) => ({ | ||
| fontSize: '2rem', | ||
| fontWeight: theme.font.weight.medium, | ||
| lineHeight: 1.3, | ||
| color: theme.colors.gray900, | ||
| })); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import styled from '@emotion/styled'; | ||
|
|
||
| export const UserInfoWrapper = styled.div(({ theme }) => ({ | ||
| boxSizing: 'border-box', | ||
| width: '48rem', | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| gap: '30px', | ||
| borderBottom: `1px solid ${theme.colors.gray200}`, | ||
| paddingBottom: '3rem', | ||
|
|
||
| '@media (max-width: 48rem)': { | ||
| width: '100%', | ||
| }, | ||
| })); | ||
|
|
||
| export const FormField = styled.div({ | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| gap: '10px', | ||
| }); | ||
|
|
||
| export const FormRow = styled.div({ | ||
| display: 'flex', | ||
| flexDirection: 'row', | ||
| gap: '2.5rem', | ||
| width: '100%', | ||
| '& > *': { | ||
| flex: '1 1 0', | ||
| minWidth: 0, | ||
| }, | ||
| }); | ||
|
|
||
| export const Label = styled.label(({ theme }) => ({ | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| gap: '4px', | ||
| fontWeight: theme.font.weight.medium, | ||
| })); | ||
|
|
||
| export const FormContainer = styled.main({ | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| alignItems: 'center', | ||
| gap: '3rem', | ||
| }); | ||
|
|
||
| export const ErrorMessage = styled.span(({ theme }) => ({ | ||
| color: theme.colors.warning, | ||
| fontSize: theme.font.size.xs, | ||
| padding: 0, | ||
| })); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,128 @@ | ||||||||||||||||||||||||||
| import { useForm, FormProvider } from 'react-hook-form'; | ||||||||||||||||||||||||||
| import { useNavigate } from 'react-router-dom'; | ||||||||||||||||||||||||||
| import { toast } from 'sonner'; | ||||||||||||||||||||||||||
| import { postSignupForm } from '@/pages/admin/Signup/api/signup'; | ||||||||||||||||||||||||||
| import * as S from '@/pages/admin/Signup/components/SignupForm/index.styled'; | ||||||||||||||||||||||||||
| import { Button } from '@/shared/components/Button'; | ||||||||||||||||||||||||||
| import { OutlineInputField } from '@/shared/components/Form/InputField/OutlineInputField'; | ||||||||||||||||||||||||||
| import { theme } from '@/styles/theme'; | ||||||||||||||||||||||||||
| import type { SignupFormInputs } from '@/pages/admin/Signup/type/signup'; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export const SignupForm = () => { | ||||||||||||||||||||||||||
| const navigate = useNavigate(); | ||||||||||||||||||||||||||
| const methods = useForm<SignupFormInputs>({ | ||||||||||||||||||||||||||
| mode: 'onTouched', | ||||||||||||||||||||||||||
| defaultValues: { | ||||||||||||||||||||||||||
| name: '', | ||||||||||||||||||||||||||
| email: '', | ||||||||||||||||||||||||||
| studentId: '', | ||||||||||||||||||||||||||
| department: '', | ||||||||||||||||||||||||||
| phoneNumber: '', | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const { errors, isSubmitting } = methods.formState; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const onSubmit = async (signupFormValue: SignupFormInputs) => { | ||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||
| await postSignupForm(signupFormValue); | ||||||||||||||||||||||||||
| toast.success('회원가입 완료!', { | ||||||||||||||||||||||||||
| style: { backgroundColor: theme.colors.primary, color: 'white' }, | ||||||||||||||||||||||||||
| duration: 1000, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| setTimeout(() => { | ||||||||||||||||||||||||||
| navigate(`/login`); | ||||||||||||||||||||||||||
| }, 1000); | ||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||
| console.error(e); | ||||||||||||||||||||||||||
| toast.error('회원가입 실패!', { | ||||||||||||||||||||||||||
| duration: 1000, | ||||||||||||||||||||||||||
| style: { | ||||||||||||||||||||||||||
| backgroundColor: 'white', | ||||||||||||||||||||||||||
| color: theme.colors.error, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
Comment on lines
26
to
46
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Comment on lines
26
to
46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix unused error variable and navigation timing issue. Two concerns:
Apply this diff to address both issues: - } catch (error) {
+ } catch (_error) {
toast.error('회원가입 실패!', {
duration: 1000,
style: {
backgroundColor: 'white',
color: theme.colors.error,
},
});
}For the navigation timing issue, consider one of these patterns: Option 1: Use toast's onAutoClose callback (cleaner) try {
await postSignupForm(signupFormValue);
toast.success('회원가입 완료!', {
style: { backgroundColor: theme.colors.primary, color: 'white' },
duration: 1000,
+ onAutoClose: () => navigate('/login'),
});
- setTimeout(() => {
- navigate(`/login`);
- }, 1000);Option 2: Store timeout ID and cleanup in useEffect useEffect(() => {
return () => {
// cleanup any pending timeouts when component unmounts
};
}, []);
// In onSubmit:
const timeoutId = setTimeout(() => navigate('/login'), 1000);
// Store timeoutId for cleanup if needed🧰 Tools🪛 GitHub Actions: Dongarium FE CI/CD[error] 36-36: ESLint: 'error' is defined but never used. (no-unused-vars). Command: npm run lint 🪛 GitHub Check: lint[failure] 36-36: |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||
| <FormProvider {...methods}> | ||||||||||||||||||||||||||
| <form onSubmit={methods.handleSubmit(onSubmit)}> | ||||||||||||||||||||||||||
| <S.FormContainer> | ||||||||||||||||||||||||||
| <S.UserInfoWrapper> | ||||||||||||||||||||||||||
| <S.FormField> | ||||||||||||||||||||||||||
| <S.Label>이름</S.Label> | ||||||||||||||||||||||||||
| <OutlineInputField | ||||||||||||||||||||||||||
| placeholder='이름을 입력하세요.' | ||||||||||||||||||||||||||
| {...methods.register('name', { required: '이름을 입력하세요.' })} | ||||||||||||||||||||||||||
| invalid={!!errors.name} | ||||||||||||||||||||||||||
| message={errors.name?.message} | ||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||
| </S.FormField> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| <S.FormField> | ||||||||||||||||||||||||||
| <S.Label>이메일</S.Label> | ||||||||||||||||||||||||||
| <OutlineInputField | ||||||||||||||||||||||||||
| placeholder='이메일을 입력하세요.' | ||||||||||||||||||||||||||
| {...methods.register('email', { | ||||||||||||||||||||||||||
| required: '이메일을 입력하세요.', | ||||||||||||||||||||||||||
| pattern: { | ||||||||||||||||||||||||||
| value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, | ||||||||||||||||||||||||||
| message: '올바른 이메일 형식이 아닙니다.', | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| })} | ||||||||||||||||||||||||||
| invalid={!!errors.email} | ||||||||||||||||||||||||||
| message={errors.email?.message} | ||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||
| </S.FormField> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| <S.FormRow> | ||||||||||||||||||||||||||
| <S.FormField> | ||||||||||||||||||||||||||
| <S.Label>학번</S.Label> | ||||||||||||||||||||||||||
| <OutlineInputField | ||||||||||||||||||||||||||
| placeholder='학번을 입력하세요.' | ||||||||||||||||||||||||||
| {...methods.register('studentId', { | ||||||||||||||||||||||||||
| required: '학번을 입력하세요.', | ||||||||||||||||||||||||||
| pattern: { | ||||||||||||||||||||||||||
| value: /^[0-9]{6}$/, | ||||||||||||||||||||||||||
| message: '학번은 숫자 6자리여야 합니다.', | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| })} | ||||||||||||||||||||||||||
| invalid={!!errors.studentId} | ||||||||||||||||||||||||||
| message={errors.studentId?.message} | ||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||
| </S.FormField> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| <S.FormField> | ||||||||||||||||||||||||||
| <S.Label>학과</S.Label> | ||||||||||||||||||||||||||
| <OutlineInputField | ||||||||||||||||||||||||||
| placeholder='학과를 입력하세요.' | ||||||||||||||||||||||||||
| {...methods.register('department', { required: '학과를 입력하세요.' })} | ||||||||||||||||||||||||||
| invalid={!!errors.department} | ||||||||||||||||||||||||||
| message={errors.department?.message} | ||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||
| </S.FormField> | ||||||||||||||||||||||||||
| </S.FormRow> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| <S.FormField> | ||||||||||||||||||||||||||
| <S.Label>전화번호</S.Label> | ||||||||||||||||||||||||||
| <OutlineInputField | ||||||||||||||||||||||||||
| placeholder='010-0000-0000' | ||||||||||||||||||||||||||
| {...methods.register('phoneNumber', { | ||||||||||||||||||||||||||
| required: '전화번호를 입력하세요.', | ||||||||||||||||||||||||||
| pattern: { | ||||||||||||||||||||||||||
| value: /^\d{2,3}-\d{3,4}-\d{4}$/, | ||||||||||||||||||||||||||
| message: '올바른 전화번호 형식이 아닙니다.', | ||||||||||||||||||||||||||
|
Comment on lines
+110
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Phone number pattern mismatch with placeholder. The regex pattern allows 2-3 digit area codes ( Apply this diff if only 3-digit area codes are intended: pattern: {
- value: /^\d{2,3}-\d{3,4}-\d{4}$/,
+ value: /^010-\d{3,4}-\d{4}$/,
message: '올바른 전화번호 형식이 아닙니다.',📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| })} | ||||||||||||||||||||||||||
| invalid={!!errors.phoneNumber} | ||||||||||||||||||||||||||
| message={errors.phoneNumber?.message} | ||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||
| </S.FormField> | ||||||||||||||||||||||||||
| </S.UserInfoWrapper> | ||||||||||||||||||||||||||
| <Button type='submit'>{isSubmitting ? '제출중...' : '회원가입'}</Button> | ||||||||||||||||||||||||||
| </S.FormContainer> | ||||||||||||||||||||||||||
| </form> | ||||||||||||||||||||||||||
| </FormProvider> | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| export type SignupFormInputs = { | ||
| name: string; | ||
| email: string; | ||
| studentId: string; | ||
| department: string; | ||
| phoneNumber: string; | ||
| }; |
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.
window.location.href를 직접 파싱하는 대신,react-router-dom에서 제공하는useSearchParams훅을 사용하면 더 선언적이고 안정적으로 쿼리 파라미터를 가져올 수 있습니다.