From 3d6f67f3dfb9970b7e7127878fa8b4165ed836da Mon Sep 17 00:00:00 2001 From: GGGGGangSub Date: Sun, 9 Nov 2025 23:45:21 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8,=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85,=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=9D=B8=EC=A6=9D=EB=B2=88=ED=98=B8=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=20=EB=B0=8F=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=9D=B8=EC=A6=9D=20api=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/login/LoginForm.jsx | 36 +++-- frontend/src/components/signup/SignUpForm.jsx | 141 +++++++++++++----- frontend/src/utils/auth.js | 53 +++++++ frontend/src/utils/axios.js | 51 +++++++ 4 files changed, 237 insertions(+), 44 deletions(-) create mode 100644 frontend/src/utils/auth.js create mode 100644 frontend/src/utils/axios.js diff --git a/frontend/src/components/login/LoginForm.jsx b/frontend/src/components/login/LoginForm.jsx index 5f93630c..91ec0406 100644 --- a/frontend/src/components/login/LoginForm.jsx +++ b/frontend/src/components/login/LoginForm.jsx @@ -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(); @@ -32,17 +34,31 @@ const LoginForm = () => { const isFormValid = email.trim() !== '' && password.trim() !== ''; - const handleLogin = (e) => { + const abortRef = useRef(null); + + 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 + ); + + console.log('로그인이 완료되었습니다.'); + nav('/'); + } catch (err) { + console.log('status:', err.status); + console.log('data:', err.data); + console.log('message:', err.message); + } }; return ( diff --git a/frontend/src/components/signup/SignUpForm.jsx b/frontend/src/components/signup/SignUpForm.jsx index 811f5572..f9d40d83 100644 --- a/frontend/src/components/signup/SignUpForm.jsx +++ b/frontend/src/components/signup/SignUpForm.jsx @@ -1,22 +1,30 @@ -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 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 [isVerificationNumberSent, setVerificationNumberSent] = useState(false); + const [isVerificationSent, setVerificationSent] = useState(false); + const [isVerificationChecked, setVerificationChecked] = useState(false); + + const abortRef = useRef(null); const nav = useNavigate(); - // 이메일 입력 형태가 맞는지 검사 + // 이메일 유효성 검사 const isEmailValid = () => { const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/; return emailRegex.test(email); @@ -32,24 +40,79 @@ const SignUpForm = () => { const isFormValid = nickname.trim() !== '' && isEmailValid() && - isPhoneNumberValid() && + isVerificationSent && + isVerificationChecked && + isPhoneNumberValid && password.trim() !== '' && password === confirmPassword; - const handleSendVerificationNumber = () => { - // 전송 state 변경 - setVerificationNumberSent(true); + const handleSendVerificationNumber = async (e) => { + e.preventDefault(); + setVerificationSent(true); + + // 도중에 요청 시 전 요청 취소 + abortRef.current?.abort(); + abortRef.current = new AbortController(); + + // 인증번호 발송 로직 & api 자리 + try { + await sendVerificationNumber({ email: email }, abortRef.current.signal); - // 인증번호 발송 로직 - alert('인증번호가 발송되었습니다.'); + alert('인증번호가 발송되었습니다.'); + } catch (err) { + console.log('status:', err.status); + console.log('data:', err.data); + console.log('message:', err.message); + } }; - const handleSignUp = (e) => { + const handleCheckVerificationNumber = async () => { + setVerificationChecked(true); + + // 도중에 요청 시 전 요청 취소 + abortRef.current?.abort(); + abortRef.current = new AbortController(); + + // 인증번호 발송 로직 & api 자리 + try { + await checkVerificationNumber( + { email: email, verificationNumber: verificationNumber }, + abortRef.current.signal + ); + + setVerificationChecked(true); + alert('인증되었습니다.'); + } catch (err) { + console.log('status:', err.status); + console.log('data:', err.data); + console.log('message:', err.message); + } + }; + + 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) { + console.log('status:', err.status); + console.log('data:', err.data); + console.log('message:', err.message); + } }; return ( @@ -78,21 +141,21 @@ const SignUpForm = () => { />
- +
setPhoneNumber(e.target.value)} - placeholder="ex) 01012345678" + id="email" + value={email} + onChange={(e) => setEmail(e.target.value)} + placeholder="ex) abcde@gmail.com" className={styles.phoneNumberInput} /> @@ -100,22 +163,32 @@ const SignUpForm = () => {
- setVerificationNumber(e.target.value)} - placeholder="인증번호를 입력해주세요" - /> +
+ setVerificationNumber(e.target.value)} + placeholder="인증번호를 입력해주세요" + /> + +
- + setEmail(e.target.value)} - placeholder="이메일을 입력해주세요" + type="text" + id="phoneNumber" + value={phoneNumber} + onChange={(e) => setPhoneNumber(e.target.value)} + placeholder="ex) 01012345678" />
diff --git a/frontend/src/utils/auth.js b/frontend/src/utils/auth.js new file mode 100644 index 00000000..f32fa21d --- /dev/null +++ b/frontend/src/utils/auth.js @@ -0,0 +1,53 @@ +import { api } from './axios.js'; + +const DEFAULT_ROLE = 'TEAM_MEMBER'; + +export const signUp = async ( + { nickname, email, password, phoneNumber }, + signal +) => { + const payload = { + name: nickname.trim(), + email: email.trim(), + password: password, + role: DEFAULT_ROLE, + phoneNumber: phoneNumber.trim(), + }; + const res = await api.post('/api/auth/signup', payload, { signal }); + return res.data; +}; + +export const login = async ({ email, password }, signal) => { + const paylaod = { email, password }; + + const res = await api.post('/api/auth/login', paylaod, { signal }); + + const { accessToken, refreshToken } = res.data; + if (accessToken && refreshToken) { + // 3. 로컬 스토리지에 각 토큰을 저장 + localStorage.setItem('accessToken', accessToken); + localStorage.setItem('refreshToken', refreshToken); + } + + return res.data; +}; + +export const sendVerificationNumber = async ({ email }, signal) => { + const res = await api.post('/api/email/send', null, { + params: { email }, + signal, + }); + + return res.data; +}; +export const checkVerificationNumber = async ( + { email, verificationNumber }, + signal +) => { + const res = await api.post('/api/email/verify', null, { + params: { email, code: verificationNumber }, + signal, + }); + + return res.data; +}; diff --git a/frontend/src/utils/axios.js b/frontend/src/utils/axios.js new file mode 100644 index 00000000..2c5d769c --- /dev/null +++ b/frontend/src/utils/axios.js @@ -0,0 +1,51 @@ +import axios from 'axios'; + +export const api = axios.create({ + baseURL: import.meta.env.VITE_API_URL, + withCredentials: true, +}); + +api.interceptors.response.use( + (res) => res, + async (err) => { + // 토큰 로직 + const originRequest = err.config; + + // 액세스 토큰 만료 확인 + if (err.response?.status === 401 && !originRequest._retry) { + originRequest._retry = true; + + try { + const rt = localStorage.getItem('refreshToken'); + const res = await axios.post('/api/auth/reissue', { refreshToken: rt }); + + const newAccessToken = res.data.accessToken; + + localStorage.setItem('accessToken', newAccessToken); + originRequest.headers.Authorization = `Bearer ${newAccessToken}`; + + return api(originRequest); + } catch (refreshError) { + console.error('Token refresh failed: ', refreshError); + localStorage.removeItem('accessToken'); + localStorage.removeItem('refreshToken'); + + window.location.href = '/login'; + return Promise.reject(refreshError); + } + } + + const status = err.response?.status; + const message = + err.response?.data?.message || + err.response?.statusText || + '오류가 발생했습니다.'; + return Promise.reject({ status, message, data: err.response?.data }); + } +); + +api.interceptors.request.use((config) => { + const at = localStorage.getItem('accessToken'); + if (at) config.headers.Authorization = `Bearer ${at}`; + return config; +}); From 0c5c31e60ce8acd8f559d6a22dafcc11802d1e78 Mon Sep 17 00:00:00 2001 From: GGGGGangSub Date: Wed, 12 Nov 2025 01:14:21 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8/?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/signup/SignUpForm.jsx | 6 ++---- frontend/src/utils/axios.js | 6 +++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/signup/SignUpForm.jsx b/frontend/src/components/signup/SignUpForm.jsx index f9d40d83..fcc5f45d 100644 --- a/frontend/src/components/signup/SignUpForm.jsx +++ b/frontend/src/components/signup/SignUpForm.jsx @@ -48,7 +48,6 @@ const SignUpForm = () => { const handleSendVerificationNumber = async (e) => { e.preventDefault(); - setVerificationSent(true); // 도중에 요청 시 전 요청 취소 abortRef.current?.abort(); @@ -58,6 +57,7 @@ const SignUpForm = () => { try { await sendVerificationNumber({ email: email }, abortRef.current.signal); + setVerificationSent(true); alert('인증번호가 발송되었습니다.'); } catch (err) { console.log('status:', err.status); @@ -66,8 +66,6 @@ const SignUpForm = () => { } }; const handleCheckVerificationNumber = async () => { - setVerificationChecked(true); - // 도중에 요청 시 전 요청 취소 abortRef.current?.abort(); abortRef.current = new AbortController(); @@ -144,7 +142,7 @@ const SignUpForm = () => {
setEmail(e.target.value)} diff --git a/frontend/src/utils/axios.js b/frontend/src/utils/axios.js index 2c5d769c..20005fb1 100644 --- a/frontend/src/utils/axios.js +++ b/frontend/src/utils/axios.js @@ -17,7 +17,11 @@ api.interceptors.response.use( try { const rt = localStorage.getItem('refreshToken'); - const res = await axios.post('/api/auth/reissue', { refreshToken: rt }); + const res = await axios.post( + `${import.meta.env.VITE_API_URL}/api/auth/reissue`, + { refreshToken: rt }, + { withCredentials: true } + ); const newAccessToken = res.data.accessToken; From f33a28be17fc979ce3b181163143de3771980935 Mon Sep 17 00:00:00 2001 From: GGGGGangSub Date: Wed, 12 Nov 2025 14:07:54 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8/?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/LoginAndSignUpForm.module.css | 16 +++++ frontend/src/components/login/LoginForm.jsx | 12 ++-- frontend/src/components/signup/SignUpForm.jsx | 64 +++++++++++++++---- 3 files changed, 74 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/LoginAndSignUpForm.module.css b/frontend/src/components/LoginAndSignUpForm.module.css index 90e8ee2d..89a9c7ad 100644 --- a/frontend/src/components/LoginAndSignUpForm.module.css +++ b/frontend/src/components/LoginAndSignUpForm.module.css @@ -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; +} diff --git a/frontend/src/components/login/LoginForm.jsx b/frontend/src/components/login/LoginForm.jsx index 91ec0406..58d52f84 100644 --- a/frontend/src/components/login/LoginForm.jsx +++ b/frontend/src/components/login/LoginForm.jsx @@ -19,7 +19,7 @@ const LoginForm = () => { const [foundEmail, setFoundEmail] = useState(''); // 전화번호 인증 성공 시 호출하는 함수 - const handlePhoneVerificationSuccess = (result) => { + const handlePhoneVerificationSuccess = () => { if (modalStep === 'verifyPhoneForEmail') { setFoundEmail('example@google.com'); setModalStep('showEmail'); @@ -52,12 +52,12 @@ const LoginForm = () => { abortRef.current.signal ); - console.log('로그인이 완료되었습니다.'); nav('/'); } catch (err) { - console.log('status:', err.status); - console.log('data:', err.data); - console.log('message:', err.message); + alert( + err.data?.message || + '로그인에 실패하였습니다. 이메일과 비밀번호를 확인해주세요.' + ); } }; @@ -85,6 +85,7 @@ const LoginForm = () => { value={email} onChange={(e) => setEmail(e.target.value)} placeholder="이메일을 입력하세요" + autoComplete="email" />
@@ -95,6 +96,7 @@ const LoginForm = () => { value={password} onChange={(e) => setPassword(e.target.value)} placeholder="비밀번호를 입력하세요" + autoComplete="current-password" />
@@ -187,6 +213,7 @@ const SignUpForm = () => { value={phoneNumber} onChange={(e) => setPhoneNumber(e.target.value)} placeholder="ex) 01012345678" + autoComplete="tel" />
@@ -195,9 +222,20 @@ const SignUpForm = () => { type="password" id="password" value={password} - onChange={(e) => setPassword(e.target.value)} + onChange={handlePasswordChange} placeholder="비밀번호를 입력해주세요" + autoComplete="new-password" /> +
From 7c3f25cbabb126ca3f810f8b83c1a99e2874f76d Mon Sep 17 00:00:00 2001 From: GGGGGangSub Date: Wed, 12 Nov 2025 14:30:06 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8/?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/login/LoginForm.jsx | 3 ++- frontend/src/components/signup/SignUpForm.jsx | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/login/LoginForm.jsx b/frontend/src/components/login/LoginForm.jsx index 58d52f84..3a00b383 100644 --- a/frontend/src/components/login/LoginForm.jsx +++ b/frontend/src/components/login/LoginForm.jsx @@ -54,8 +54,9 @@ const LoginForm = () => { nav('/'); } catch (err) { + console.dir(err); alert( - err.data?.message || + err.data?.errorMessage || '로그인에 실패하였습니다. 이메일과 비밀번호를 확인해주세요.' ); } diff --git a/frontend/src/components/signup/SignUpForm.jsx b/frontend/src/components/signup/SignUpForm.jsx index 7ac1c28c..7067a3b0 100644 --- a/frontend/src/components/signup/SignUpForm.jsx +++ b/frontend/src/components/signup/SignUpForm.jsx @@ -86,7 +86,7 @@ const SignUpForm = () => { setVerificationSent(true); alert('인증번호가 발송되었습니다.'); } catch (err) { - alert(err.data?.message || '전송 오류가 발생했습니다.'); + alert(err.data?.errorMessage || '전송 오류가 발생했습니다.'); } finally { setIsSending(false); } @@ -106,7 +106,7 @@ const SignUpForm = () => { setVerificationChecked(true); alert('인증되었습니다.'); } catch (err) { - alert(err.data?.message || '인증에 실패했습니다.'); + alert(err.response?.data?.message || '인증에 실패했습니다.'); } }; @@ -131,7 +131,7 @@ const SignUpForm = () => { alert('회원가입이 완료되었습니다.'); nav('/login'); } catch (err) { - alert(err.data?.message || '회원가입에 실패하였습니다.'); + alert(err.data?.errorMessage || '회원가입에 실패하였습니다.'); } };