-
Notifications
You must be signed in to change notification settings - Fork 2
[FE] SISC1-195 [FEAT] 로그인, 회원가입, 이메일 인증번호 전송 및 이메일 번호 인증 api 연동 #90
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
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 | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,4 @@ | ||||||||||||||||||||
| import { useState } from 'react'; | ||||||||||||||||||||
| import { useRef, useState } from 'react'; | ||||||||||||||||||||
| import { useNavigate, NavLink } from 'react-router-dom'; | ||||||||||||||||||||
| import styles from '../LoginAndSignUpForm.module.css'; | ||||||||||||||||||||
| import sejong_logo from '../../assets/sejong_logo.png'; | ||||||||||||||||||||
|
|
@@ -8,6 +8,8 @@ import VerificationModal from './../VerificationModal'; | |||||||||||||||||||
| import ResetPasswordModal from './ResetPasswordModal'; | ||||||||||||||||||||
| import FindEmailResultModal from './FindEmailResultModal'; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import { login } from '../../utils/auth'; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const LoginForm = () => { | ||||||||||||||||||||
| const nav = useNavigate(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -17,7 +19,7 @@ const LoginForm = () => { | |||||||||||||||||||
| const [foundEmail, setFoundEmail] = useState(''); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // 전화번호 인증 성공 시 호출하는 함수 | ||||||||||||||||||||
| const handlePhoneVerificationSuccess = (result) => { | ||||||||||||||||||||
| const handlePhoneVerificationSuccess = () => { | ||||||||||||||||||||
| if (modalStep === 'verifyPhoneForEmail') { | ||||||||||||||||||||
| setFoundEmail('example@google.com'); | ||||||||||||||||||||
| setModalStep('showEmail'); | ||||||||||||||||||||
|
|
@@ -32,17 +34,32 @@ const LoginForm = () => { | |||||||||||||||||||
|
|
||||||||||||||||||||
| const isFormValid = email.trim() !== '' && password.trim() !== ''; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const handleLogin = (e) => { | ||||||||||||||||||||
| const abortRef = useRef(null); | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
Comment on lines
+37
to
+38
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. 컴포넌트 언마운트 시 진행 중인 요청을 취소하는 cleanup이 필요합니다.
다음 useEffect를 추가하여 cleanup을 구현하세요: const abortRef = useRef(null);
+
+useEffect(() => {
+ return () => {
+ abortRef.current?.abort();
+ };
+}, []);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| const handleLogin = async (e) => { | ||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||
| // 안전장치 | ||||||||||||||||||||
| if (!email || !password) { | ||||||||||||||||||||
| alert('이메일과 비밀번호를 모두 입력해주세요.'); | ||||||||||||||||||||
| return; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // 로그인 성공 시 로직 | ||||||||||||||||||||
| localStorage.setItem('authToken', 'dummy-token-12345'); | ||||||||||||||||||||
| nav('/'); | ||||||||||||||||||||
| // 도중에 요청 시 전 요청 취소 | ||||||||||||||||||||
| abortRef.current?.abort(); | ||||||||||||||||||||
| abortRef.current = new AbortController(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| try { | ||||||||||||||||||||
| await login( | ||||||||||||||||||||
| { | ||||||||||||||||||||
| email, | ||||||||||||||||||||
| password, | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| abortRef.current.signal | ||||||||||||||||||||
| ); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| nav('/'); | ||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||
| console.dir(err); | ||||||||||||||||||||
| alert( | ||||||||||||||||||||
| err.data?.errorMessage || | ||||||||||||||||||||
| '로그인에 실패하였습니다. 이메일과 비밀번호를 확인해주세요.' | ||||||||||||||||||||
| ); | ||||||||||||||||||||
|
Comment on lines
+58
to
+61
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. Axios 에러 구조가 잘못되어 서버 에러 메시지가 표시되지 않습니다. Axios에서 발생한 에러의 응답 데이터는 다음과 같이 수정하세요: } catch (err) {
alert(
- err.data?.message ||
+ err.response?.data?.message ||
'로그인에 실패하였습니다. 이메일과 비밀번호를 확인해주세요.'
);
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| } | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| 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. 88번째 줄에 이메일 input 태그에 autoComplete="email" 추가하라고 크롬에서 알려주고 있는데 저도 이거에 대해선 잘 몰라서 한 번 찾아보시면 좋을 것 같습니다 |
||||||||||||||||||||
|
|
@@ -69,6 +86,7 @@ const LoginForm = () => { | |||||||||||||||||||
| value={email} | ||||||||||||||||||||
| onChange={(e) => setEmail(e.target.value)} | ||||||||||||||||||||
| placeholder="이메일을 입력하세요" | ||||||||||||||||||||
| autoComplete="email" | ||||||||||||||||||||
| /> | ||||||||||||||||||||
| </div> | ||||||||||||||||||||
| <div className={styles.inputGroup}> | ||||||||||||||||||||
|
|
@@ -79,6 +97,7 @@ const LoginForm = () => { | |||||||||||||||||||
| value={password} | ||||||||||||||||||||
| onChange={(e) => setPassword(e.target.value)} | ||||||||||||||||||||
| placeholder="비밀번호를 입력하세요" | ||||||||||||||||||||
| autoComplete="current-password" | ||||||||||||||||||||
| /> | ||||||||||||||||||||
| </div> | ||||||||||||||||||||
| <button | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,22 +1,52 @@ | ||||||||||
| import { useState } from 'react'; | ||||||||||
| import { useRef, useState } from 'react'; | ||||||||||
| import { useNavigate } from 'react-router-dom'; | ||||||||||
| import styles from '../LoginAndSignUpForm.module.css'; | ||||||||||
| import sejong_logo from '../../assets/sejong_logo.png'; | ||||||||||
| import EmailVerificationModal from './../VerificationModal'; | ||||||||||
|
|
||||||||||
| import { | ||||||||||
| sendVerificationNumber, | ||||||||||
| signUp, | ||||||||||
| checkVerificationNumber, | ||||||||||
| } from '../../utils/auth.js'; | ||||||||||
|
|
||||||||||
| const passwordPolicy = [ | ||||||||||
| { label: '8~20자 이내', test: (pw) => pw.length >= 8 && pw.length <= 20 }, | ||||||||||
| { label: '최소 1개의 대문자 포함', test: (pw) => /[A-Z]/.test(pw) }, | ||||||||||
| { label: '최소 1개의 소문자 포함', test: (pw) => /[a-z]/.test(pw) }, | ||||||||||
| { label: '최소 1개의 숫자 포함', test: (pw) => /[0-9]/.test(pw) }, | ||||||||||
| { label: '최소 1개의 특수문자 포함', test: (pw) => /[\W_]/.test(pw) }, | ||||||||||
| ]; | ||||||||||
|
|
||||||||||
| const SignUpForm = () => { | ||||||||||
| const [nickname, setNickname] = useState(''); | ||||||||||
| const [phoneNumber, setPhoneNumber] = useState(''); | ||||||||||
| const [verificationNumber, setVerificationNumber] = useState(''); | ||||||||||
| const [email, setEmail] = useState(''); | ||||||||||
| const [phoneNumber, setPhoneNumber] = useState(''); | ||||||||||
| const [password, setPassword] = useState(''); | ||||||||||
| const [confirmPassword, setConfirmPassword] = useState(''); | ||||||||||
| const [passwordValid, setPasswordValid] = useState( | ||||||||||
| Array(passwordPolicy.length).fill(false) | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| const [isVerificationNumberSent, setVerificationNumberSent] = useState(false); | ||||||||||
| const [isSending, setIsSending] = useState(false); | ||||||||||
|
|
||||||||||
| const [isVerificationSent, setVerificationSent] = useState(false); | ||||||||||
| const [isVerificationChecked, setVerificationChecked] = useState(false); | ||||||||||
|
|
||||||||||
| const abortRef = useRef(null); | ||||||||||
|
|
||||||||||
| const nav = useNavigate(); | ||||||||||
|
|
||||||||||
| // 이메일 입력 형태가 맞는지 검사 | ||||||||||
| const handlePasswordChange = (e) => { | ||||||||||
| const newPassword = e.target.value; | ||||||||||
| setPassword(newPassword); | ||||||||||
| const newPasswordValid = passwordPolicy.map((rule) => | ||||||||||
| rule.test(newPassword) | ||||||||||
| ); | ||||||||||
| setPasswordValid(newPasswordValid); | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| // 이메일 유효성 검사 | ||||||||||
| const isEmailValid = () => { | ||||||||||
| const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/; | ||||||||||
| return emailRegex.test(email); | ||||||||||
|
|
@@ -29,27 +59,80 @@ const SignUpForm = () => { | |||||||||
| }; | ||||||||||
|
|
||||||||||
| // 회원가입 제출 유효성 검사 | ||||||||||
| const isPasswordValid = passwordValid.every(Boolean); | ||||||||||
|
|
||||||||||
| const isFormValid = | ||||||||||
| nickname.trim() !== '' && | ||||||||||
| isEmailValid() && | ||||||||||
| isPhoneNumberValid() && | ||||||||||
| password.trim() !== '' && | ||||||||||
| isVerificationSent && | ||||||||||
| isVerificationChecked && | ||||||||||
| isPhoneNumberValid && | ||||||||||
| isPasswordValid && | ||||||||||
| password === confirmPassword; | ||||||||||
|
|
||||||||||
| const handleSendVerificationNumber = () => { | ||||||||||
| // 전송 state 변경 | ||||||||||
| setVerificationNumberSent(true); | ||||||||||
| const handleSendVerificationNumber = async (e) => { | ||||||||||
| e.preventDefault(); | ||||||||||
|
|
||||||||||
| // 도중에 요청 시 전 요청 취소 | ||||||||||
| abortRef.current?.abort(); | ||||||||||
| abortRef.current = new AbortController(); | ||||||||||
|
|
||||||||||
| setIsSending(true); | ||||||||||
|
|
||||||||||
| // 인증번호 발송 로직 & api 자리 | ||||||||||
| try { | ||||||||||
| await sendVerificationNumber({ email: email }, abortRef.current.signal); | ||||||||||
|
|
||||||||||
| setVerificationSent(true); | ||||||||||
| alert('인증번호가 발송되었습니다.'); | ||||||||||
| } catch (err) { | ||||||||||
| alert(err.data?.errorMessage || '전송 오류가 발생했습니다.'); | ||||||||||
|
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. Axios 에러 구조가 잘못되었습니다.
다음과 같이 수정하세요: } catch (err) {
- alert(err.data?.errorMessage || '전송 오류가 발생했습니다.');
+ alert(err.response?.data?.message || '전송 오류가 발생했습니다.');
} finally {🤖 Prompt for AI Agents |
||||||||||
| } finally { | ||||||||||
| setIsSending(false); | ||||||||||
| } | ||||||||||
| }; | ||||||||||
|
Comment on lines
+73
to
+93
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. 재전송 시 인증 확인 상태를 초기화하지 않는 보안 취약점이 있습니다. 현재 코드에서 사용자가 인증번호를 재전송할 때
이전 리뷰에서도 지적된 사항이나 아직 수정되지 않았습니다. 다음 수정을 적용하세요: const handleSendVerificationNumber = async (e) => {
e.preventDefault();
+ // 재전송 시 이전 인증 상태 초기화
+ setVerificationChecked(false);
+
// 도중에 요청 시 전 요청 취소
abortRef.current?.abort();
abortRef.current = new AbortController();
setIsSending(true);
// 인증번호 발송 로직 & api 자리
try {
await sendVerificationNumber({ email: email }, abortRef.current.signal);
setVerificationSent(true);
alert('인증번호가 발송되었습니다.');
} catch (err) {
alert(err.data?.message || '전송 오류가 발생했습니다.');
} finally {
setIsSending(false);
}
};🤖 Prompt for AI Agents |
||||||||||
| const handleCheckVerificationNumber = async () => { | ||||||||||
| // 도중에 요청 시 전 요청 취소 | ||||||||||
| abortRef.current?.abort(); | ||||||||||
| abortRef.current = new AbortController(); | ||||||||||
|
|
||||||||||
| // 인증번호 발송 로직 & api 자리 | ||||||||||
| try { | ||||||||||
| await checkVerificationNumber( | ||||||||||
| { email: email, verificationNumber: verificationNumber }, | ||||||||||
| abortRef.current.signal | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| // 인증번호 발송 로직 | ||||||||||
| alert('인증번호가 발송되었습니다.'); | ||||||||||
| setVerificationChecked(true); | ||||||||||
| alert('인증되었습니다.'); | ||||||||||
| } catch (err) { | ||||||||||
| alert(err.response?.data?.message || '인증에 실패했습니다.'); | ||||||||||
| } | ||||||||||
| }; | ||||||||||
| const handleSignUp = (e) => { | ||||||||||
|
|
||||||||||
| const handleSignUp = async (e) => { | ||||||||||
| e.preventDefault(); | ||||||||||
|
|
||||||||||
| // api 자리 | ||||||||||
| // 도중에 요청 시 전 요청 취소 | ||||||||||
| abortRef.current?.abort(); | ||||||||||
| abortRef.current = new AbortController(); | ||||||||||
|
|
||||||||||
| try { | ||||||||||
| await signUp( | ||||||||||
| { | ||||||||||
| nickname, | ||||||||||
| email, | ||||||||||
| password, | ||||||||||
| phoneNumber, | ||||||||||
| }, | ||||||||||
| abortRef.current.signal | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| // localStorage.setItem('authToken', 'dummy-token-12345'); | ||||||||||
| nav('/login'); // 회원가입 성공 시 로그인 페이지 이동 | ||||||||||
| alert('회원가입이 완료되었습니다.'); | ||||||||||
| nav('/login'); | ||||||||||
| } catch (err) { | ||||||||||
| alert(err.data?.errorMessage || '회원가입에 실패하였습니다.'); | ||||||||||
|
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. Axios 에러 구조가 잘못되었습니다.
다음과 같이 수정하세요: } catch (err) {
- alert(err.data?.errorMessage || '회원가입에 실패하였습니다.');
+ alert(err.response?.data?.message || '회원가입에 실패하였습니다.');
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| } | ||||||||||
| }; | ||||||||||
|
Comment on lines
+113
to
136
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. 컴포넌트 언마운트 시 cleanup이 누락되었습니다.
다음과 같이 cleanup useEffect를 추가하세요: const abortRef = useRef(null);
+
+useEffect(() => {
+ return () => {
+ abortRef.current?.abort();
+ };
+}, []);
const nav = useNavigate();
🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| return ( | ||||||||||
|
|
@@ -78,44 +161,59 @@ const SignUpForm = () => { | |||||||||
| /> | ||||||||||
| </div> | ||||||||||
| <div className={styles.inputGroup}> | ||||||||||
| <label htmlFor="phoneNumber">휴대전화</label> | ||||||||||
| <label htmlFor="email">Email</label> | ||||||||||
| <div className={styles.phoneVerificationContainer}> | ||||||||||
| <input | ||||||||||
| type="phoneNumber" | ||||||||||
| id="text" | ||||||||||
| value={phoneNumber} | ||||||||||
| onChange={(e) => setPhoneNumber(e.target.value)} | ||||||||||
| placeholder="ex) 01012345678" | ||||||||||
| type="email" | ||||||||||
| id="email" | ||||||||||
| value={email} | ||||||||||
| onChange={(e) => setEmail(e.target.value)} | ||||||||||
| placeholder="ex) abcde@gmail.com" | ||||||||||
| className={styles.phoneNumberInput} | ||||||||||
| /> | ||||||||||
| <button | ||||||||||
| type="button" | ||||||||||
| className={styles.verifyButton} | ||||||||||
| onClick={handleSendVerificationNumber} | ||||||||||
| disabled={!isPhoneNumberValid()} | ||||||||||
| disabled={!isEmailValid() || isSending} | ||||||||||
| > | ||||||||||
| 인증번호 발송 | ||||||||||
| {isSending | ||||||||||
| ? '전송 중...' | ||||||||||
| : isVerificationSent | ||||||||||
| ? '재전송' | ||||||||||
| : '인증번호 발송'} | ||||||||||
| </button> | ||||||||||
| </div> | ||||||||||
| </div> | ||||||||||
| <div className={styles.inputGroup}> | ||||||||||
| <label htmlFor="verificationNumber">인증번호</label> | ||||||||||
| <input | ||||||||||
| type="text" | ||||||||||
| id="verificationNumber" | ||||||||||
| value={verificationNumber} | ||||||||||
| onChange={(e) => setVerificationNumber(e.target.value)} | ||||||||||
| placeholder="인증번호를 입력해주세요" | ||||||||||
| /> | ||||||||||
| <div className={styles.phoneVerificationContainer}> | ||||||||||
| <input | ||||||||||
| type="text" | ||||||||||
| id="verificationNumber" | ||||||||||
| value={verificationNumber} | ||||||||||
| onChange={(e) => setVerificationNumber(e.target.value)} | ||||||||||
| placeholder="인증번호를 입력해주세요" | ||||||||||
| /> | ||||||||||
| <button | ||||||||||
| type="button" | ||||||||||
| className={styles.verifyButton} | ||||||||||
| onClick={handleCheckVerificationNumber} | ||||||||||
| disabled={!isVerificationSent} | ||||||||||
| > | ||||||||||
| 인증번호 확인 | ||||||||||
| </button> | ||||||||||
| </div> | ||||||||||
| </div> | ||||||||||
| <div className={styles.inputGroup}> | ||||||||||
| <label htmlFor="email">Email</label> | ||||||||||
| <label htmlFor="phoneNumber">전화번호</label> | ||||||||||
| <input | ||||||||||
| type="email" | ||||||||||
| id="email" | ||||||||||
| value={email} | ||||||||||
| onChange={(e) => setEmail(e.target.value)} | ||||||||||
| placeholder="이메일을 입력해주세요" | ||||||||||
| type="text" | ||||||||||
| id="phoneNumber" | ||||||||||
| value={phoneNumber} | ||||||||||
| onChange={(e) => setPhoneNumber(e.target.value)} | ||||||||||
| placeholder="ex) 01012345678" | ||||||||||
|
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. autoComplete="tel"
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. 199번째 줄과 209번째 줄은 autoComplete="new-password" |
||||||||||
| autoComplete="tel" | ||||||||||
| /> | ||||||||||
| </div> | ||||||||||
| <div className={styles.inputGroup}> | ||||||||||
|
|
@@ -124,9 +222,20 @@ const SignUpForm = () => { | |||||||||
| type="password" | ||||||||||
| id="password" | ||||||||||
| value={password} | ||||||||||
| onChange={(e) => setPassword(e.target.value)} | ||||||||||
| onChange={handlePasswordChange} | ||||||||||
| placeholder="비밀번호를 입력해주세요" | ||||||||||
| autoComplete="new-password" | ||||||||||
| /> | ||||||||||
| <ul className={styles.passwordPolicy}> | ||||||||||
| {passwordPolicy.map((rule, index) => ( | ||||||||||
| <li | ||||||||||
| key={rule.label} | ||||||||||
| className={passwordValid[index] ? styles.valid : ''} | ||||||||||
| > | ||||||||||
| {rule.label} | ||||||||||
| </li> | ||||||||||
| ))} | ||||||||||
| </ul> | ||||||||||
| </div> | ||||||||||
| <div className={styles.inputGroup}> | ||||||||||
| <label htmlFor="confirm-password">비밀번호 확인</label> | ||||||||||
|
|
||||||||||
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.
22번째 줄 handlePhoneVerificationSuccess 메서드가 인자로 result를 받고 있는데 사용하질 않아서 지워주시면 되겠습니다.