Skip to content
Merged
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
16 changes: 16 additions & 0 deletions frontend/src/components/LoginAndSignUpForm.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,19 @@
background-color: #bce5ff;
cursor: not-allowed;
}

.passwordPolicy {
list-style: none;
padding: 0;
margin: 8px 0px 0px 8px;
font-size: 13px;
color: #868e96;
}

.passwordPolicy li {
transition: color 0.2s ease-in-out;
}

.passwordPolicy li.valid {
color: #28a745;
}
41 changes: 30 additions & 11 deletions frontend/src/components/login/LoginForm.jsx
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';
Expand All @@ -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();

Expand All @@ -17,7 +19,7 @@ const LoginForm = () => {
const [foundEmail, setFoundEmail] = useState('');

// 전화번호 인증 성공 시 호출하는 함수
const handlePhoneVerificationSuccess = (result) => {
const handlePhoneVerificationSuccess = () => {
if (modalStep === 'verifyPhoneForEmail') {
setFoundEmail('example@google.com');
setModalStep('showEmail');
Expand All @@ -32,17 +34,32 @@ const LoginForm = () => {

Copy link
Contributor

Choose a reason for hiding this comment

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

22번째 줄 handlePhoneVerificationSuccess 메서드가 인자로 result를 받고 있는데 사용하질 않아서 지워주시면 되겠습니다.

const isFormValid = email.trim() !== '' && password.trim() !== '';

const handleLogin = (e) => {
const abortRef = useRef(null);

Comment on lines +37 to +38
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

컴포넌트 언마운트 시 진행 중인 요청을 취소하는 cleanup이 필요합니다.

abortRef를 사용하여 요청 취소를 관리하고 있지만, 컴포넌트가 언마운트될 때 진행 중인 요청을 취소하는 로직이 없습니다. 이는 메모리 누수나 "unmounted component에서 state 업데이트" 경고를 유발할 수 있습니다.

다음 useEffect를 추가하여 cleanup을 구현하세요:

 const abortRef = useRef(null);
+
+useEffect(() => {
+  return () => {
+    abortRef.current?.abort();
+  };
+}, []);
📝 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 abortRef = useRef(null);
const abortRef = useRef(null);
useEffect(() => {
return () => {
abortRef.current?.abort();
};
}, []);
🤖 Prompt for AI Agents
In frontend/src/components/login/LoginForm.jsx around lines 37 to 38, you must
add a useEffect cleanup that aborts any in-flight request via abortRef: on mount
return a cleanup function that checks if abortRef.current is an AbortController
and calls abort(), then clears abortRef.current; ensure any place creating a new
AbortController assigns it to abortRef.current so the cleanup can cancel ongoing
requests and prevent state updates after unmount.

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
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Axios 에러 구조가 잘못되어 서버 에러 메시지가 표시되지 않습니다.

Axios에서 발생한 에러의 응답 데이터는 err.response.data에 있지만, 현재 코드는 err.data?.message에 접근하고 있습니다. 이로 인해 서버에서 전달한 에러 메시지가 표시되지 않고 항상 기본 메시지만 표시됩니다.

다음과 같이 수정하세요:

 } catch (err) {
   alert(
-    err.data?.message ||
+    err.response?.data?.message ||
       '로그인에 실패하였습니다. 이메일과 비밀번호를 확인해주세요.'
   );
 }
📝 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
alert(
err.data?.message ||
'로그인에 실패하였습니다. 이메일과 비밀번호를 확인해주세요.'
);
alert(
err.response?.data?.message ||
'로그인에 실패하였습니다. 이메일과 비밀번호를 확인해주세요.'
);
🤖 Prompt for AI Agents
In frontend/src/components/login/LoginForm.jsx around lines 57 to 60, the error
handling accesses err.data?.message which is incorrect for Axios errors; change
it to read the server message from err.response?.data?.message (with optional
chaining) and fall back to the existing default string — i.e., use
err.response?.data?.message || '로그인에 실패하였습니다. 이메일과 비밀번호를 확인해주세요.' so the
server-provided message is shown when available.

}
};

return (
Copy link
Contributor

Choose a reason for hiding this comment

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

88번째 줄에 이메일 input 태그에 autoComplete="email" 추가하라고 크롬에서 알려주고 있는데 저도 이거에 대해선 잘 몰라서 한 번 찾아보시면 좋을 것 같습니다
98번째 줄 또한 autoComplete="current-password"을 추가하라고 하네요

Expand All @@ -69,6 +86,7 @@ const LoginForm = () => {
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="이메일을 입력하세요"
autoComplete="email"
/>
</div>
<div className={styles.inputGroup}>
Expand All @@ -79,6 +97,7 @@ const LoginForm = () => {
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="비밀번호를 입력하세요"
autoComplete="current-password"
/>
</div>
<button
Expand Down
185 changes: 147 additions & 38 deletions frontend/src/components/signup/SignUpForm.jsx
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);
Expand All @@ -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 || '전송 오류가 발생했습니다.');
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Axios 에러 구조가 잘못되었습니다.

err.data?.errorMessage는 Axios 에러 구조가 아닙니다. 서버 응답은 err.response?.data에 있습니다.

다음과 같이 수정하세요:

 } catch (err) {
-  alert(err.data?.errorMessage || '전송 오류가 발생했습니다.');
+  alert(err.response?.data?.message || '전송 오류가 발생했습니다.');
 } finally {
🤖 Prompt for AI Agents
In frontend/src/components/signup/SignUpForm.jsx around line 89, the alert uses
a non-Axios error path (err.data?.errorMessage); change it to read the server
message from err.response?.data (e.g., err.response?.data?.errorMessage) and
fall back to the default message, so capture the error message from Axios
response properly before alerting.

} finally {
setIsSending(false);
}
};
Comment on lines +73 to +93
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

재전송 시 인증 확인 상태를 초기화하지 않는 보안 취약점이 있습니다.

현재 코드에서 사용자가 인증번호를 재전송할 때 isVerificationChecked 상태를 초기화하지 않아 다음과 같은 보안 문제가 발생합니다:

  1. 사용자가 인증번호를 받아 성공적으로 인증 (→ isVerificationChecked = true)
  2. 사용자가 새 인증번호 재전송 요청
  3. 새 인증번호를 확인하지 않았는데도 isVerificationChecked가 여전히 true로 유지됨
  4. 폼 제출이 가능해져 실제로는 새 코드를 검증하지 않은 채 회원가입 진행 가능

이전 리뷰에서도 지적된 사항이나 아직 수정되지 않았습니다.

다음 수정을 적용하세요:

   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
In frontend/src/components/signup/SignUpForm.jsx around lines 73 to 93, the
resend verification flow doesn't reset the verification-checked state, allowing
a previously successful verification to remain true after requesting a new code;
update the handler to explicitly reset verification state before sending (e.g.,
setIsVerificationChecked(false) and optionally clear the verification code
input/state such as setVerificationCode('')), then proceed with aborting
previous request and sending the new verification number so the user must verify
the newly issued code.

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 || '회원가입에 실패하였습니다.');
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Axios 에러 구조가 잘못되었습니다.

err.data?.errorMessage는 Axios 에러 구조가 아닙니다. 서버 응답은 err.response?.data에 있습니다.

다음과 같이 수정하세요:

 } catch (err) {
-  alert(err.data?.errorMessage || '회원가입에 실패하였습니다.');
+  alert(err.response?.data?.message || '회원가입에 실패하였습니다.');
 }
📝 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
alert(err.data?.errorMessage || '회원가입에 실패하였습니다.');
} catch (err) {
alert(err.response?.data?.message || '회원가입에 실패하였습니다.');
}
🤖 Prompt for AI Agents
In frontend/src/components/signup/SignUpForm.jsx around line 134, the error
handling uses err.data?.errorMessage which is not the Axios error shape; change
it to read the server message from err.response?.data?.errorMessage (with a safe
optional chaining fallback to a generic message) so the alert shows the real
server error when available.

}
};
Comment on lines +113 to 136
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

컴포넌트 언마운트 시 cleanup이 누락되었습니다.

abortRef를 사용하여 요청 취소를 관리하고 있지만, 컴포넌트가 언마운트될 때 진행 중인 요청을 취소하는 로직이 없습니다.

다음과 같이 cleanup useEffect를 추가하세요:

 const abortRef = useRef(null);
+
+useEffect(() => {
+  return () => {
+    abortRef.current?.abort();
+  };
+}, []);
 
 const nav = useNavigate();

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

🤖 Prompt for AI Agents
In frontend/src/components/signup/SignUpForm.jsx around lines 113 to 136, the
component uses abortRef to cancel in-flight signup requests but lacks a cleanup
on unmount; add a useEffect that returns a cleanup function which calls
abortRef.current?.abort() (and optionally sets abortRef.current = null) to
ensure any pending request is aborted when the component unmounts; place this
useEffect inside the component so it runs once on mount/unmount (empty deps).


return (
Expand Down Expand Up @@ -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"
Copy link
Contributor

Choose a reason for hiding this comment

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

autoComplete="tel"

Copy link
Contributor

Choose a reason for hiding this comment

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

199번째 줄과 209번째 줄은 autoComplete="new-password"

autoComplete="tel"
/>
</div>
<div className={styles.inputGroup}>
Expand All @@ -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>
Expand Down
Loading