Merge branch 'FE/PasswordResetPage' into 'frontend'

[Front-End] feat: passwordResetAuthPage 추가 외

See merge request s11-webmobile1-sub2/S11P12A701!96
This commit is contained in:
조현수 2024-08-07 17:57:16 +09:00
commit ff39d1475c
10 changed files with 278 additions and 97 deletions

View File

@ -0,0 +1,49 @@
import styles from './QuizCard.module.css';
import { STATIC_URL } from '../../constants';
export default function QuizCard({ index, question, answer, image, choices }) {
console.log(question, answer, image, choices);
return (
<div className={styles.card}>
<div className={styles.header}>
<span className={styles.heading}>{index} 퀴즈</span>
</div>
{image ? (
<img
src={`${STATIC_URL}${image}`}
alt="이미지 없음"
className={styles.imagePreview}
/>
) : (
<div className={styles.imagePreview}>
<div>이미지 없음</div>
</div>
)}
<label className={styles.label}>질문</label>
<input
type="text"
value={question}
className={styles.input}
/>
<label className={styles.label}>정답</label>
<input
type="text"
value={answer}
className={styles.input}
/>
{choices.map?.((choice, idx) => (
<div
className={styles.choiceDiv}
key={idx}
>
<label>선택지 {choice.num} </label>
<input
className={`${styles.input} ${styles.choiceInput}`}
type="text"
value={choice.content}
/>
</div>
))}
</div>
);
}

View File

@ -0,0 +1,111 @@
.card {
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px 12px;
width: 416px;
display: flex;
flex-direction: column;
gap: 8px;
}
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.heading {
font-size: 24px;
line-height: 1.2;
font-weight: 700;
}
.label {
color: var(--text-color);
font-size: 14px;
line-height: 1.4;
font-weight: 400;
margin-bottom: 4px;
}
.imagePreview {
display: flex;
justify-content: center;
align-items: center;
width: 295px;
height: 220px;
margin: 10px auto;
border-radius: 8px;
background-color: var(--background-secondary);
cursor: pointer;
}
.hiddenInput {
display: none;
}
.input {
padding: 14px;
background: var(--background);
border: 1px solid var(--border-color);
border-radius: 8px;
font-size: 14px;
line-height: 1.4;
font-weight: 400;
}
.choiceDiv {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.choiceInput {
flex-grow: 1;
padding: 7px;
}
.input::placeholder {
color: var(--text-color-tertiary);
}
.buttonsWrapper {
display: flex;
flex-direction: row;
gap: 8px;
}
.button {
display: flex;
align-items: center;
padding: 12px 16px;
font-size: 16px;
line-height: 1.4;
font-weight: 700;
align-self: end;
border-radius: 8px;
cursor: pointer;
}
.add {
border: 1px solid var(--primary-color);
background-color: var(--primary-color);
color: var(--on-primary);
stroke: var(--on-primary);
}
.remove {
border: 1px solid var(--blue100);
background-color: var(--blue100);
color: var(--info-color);
stroke: var(--info-color);
}
.cardRemove {
border: 1px solid var(--background-secondary);
background-color: var(--background-secondary);
color: var(--text-color-secondary);
stroke: var(--text-color-secondary);
}

View File

@ -1,2 +1,3 @@
export { default as QuizCard } from './QuizCard';
export { default as QuizsetForm } from './QuizsetForm';
export { default as QuizDetailCard } from './QuizDetailCard';

View File

@ -1,9 +1,10 @@
import BackIcon from '/src/assets/icons/back.svg?react';
import { Link } from 'react-router-dom';
import styles from './QuizsetDetail.module.css';
import { STATIC_URL } from '../../constants';
import { QuizDetailCard } from '../QuizForm';
export default function QuizsetDetail({ topic, title, quizzes = [], onDelete, onEdit }) {
console.log('topic', topic, 'title', title, 'quizzes', quizzes);
return (
<div className={styles.quizsetDetail}>
<header className={styles.header}>
@ -18,27 +19,16 @@ export default function QuizsetDetail({ topic, title, quizzes = [], onDelete, on
<h1 className={styles.title}>{title}</h1>
</div>
</header>
<div>
{quizzes.map((quiz, index) => (
<div key={index}>
<div>질문 : {quiz.question}</div>
{quiz.image && (
<img
src={`${STATIC_URL}${quiz.image}`}
alt="강의 이미지"
className={styles.image}
/>
)}
<div>정답 : {quiz.answer}</div>
{quiz.choices != [] &&
quiz.choices.map?.((choice, choiceIndex) => (
<div key={choice.id}>
<div>
선택지 {choiceIndex + 1} : {choice.content}
</div>
</div>
))}
</div>
<div className={styles.grid}>
{quizzes.map?.((quiz, index) => (
<QuizDetailCard
key={index}
index={index + 1}
question={quiz.question}
answer={quiz.answer}
choices={quiz.choices}
image={quiz.image}
/>
))}
</div>
<button

View File

@ -42,3 +42,11 @@
border-radius: 8px;
background-color: var(--background-secondary);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, 440px);
gap: 20px;
justify-content: start;
margin-bottom: 40px;
}

View File

@ -1,15 +0,0 @@
import instance from '../../utils/axios/instance';
import { API_URL } from '../../constants';
export function usePasswordChange() {
const passwordChange = (currentPw, newPw, newPwCheck) => {
const newPasswordBody = {
currentPassword: currentPw,
newPassword: newPw,
newPasswordCheck: newPwCheck,
};
return instance.put(`${API_URL}/user/updatepassword/`, newPasswordBody);
};
return { passwordChange };
}

View File

@ -0,0 +1,19 @@
import instance from '../../utils/axios/instance';
import { API_URL } from '../../constants';
export function usePasswordReset() {
const sendEmail = (email) => {
console.log(email);
return instance.post(`${API_URL}/mail/sendcode?email=${email}`);
};
const verify = (authNum, email) => {
return instance.get(`${API_URL}/mail/verify?code=${authNum}&email=${email}`);
};
const updatePassword = (newPassword, email) => {
return instance.put(`${API_URL}/user/updateforgottenpassword?email=${email}&newPassword=${newPassword}`);
};
return { sendEmail, verify, updatePassword };
}

View File

@ -1,20 +1,39 @@
import { AuthForm, InputBox } from '../../components/AuthForm';
import { useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import styles from './PasswordResetAuthPage.module.css';
import { usePasswordReset } from '../../hooks/api/usePasswordReset';
export default function PasswordResetPage() {
// TODO:
const location = useLocation();
const email = location.state;
const navigate = useNavigate();
const { verify, updatePassword } = usePasswordReset();
const [sentAuthNum, setSentAuthNum] = useState(false);
const authNumRef = useRef('');
const passwordRef = useRef('');
const passwordConfirmRef = useRef('');
const [passwordMatch, setPasswordMatch] = useState(true);
const [authError, setAuthError] = useState(false);
const handleSubmit = (e) => {
e.preventDefault();
console.log(authNumRef.current.value);
authNumRef.current.value = '';
setSentAuthNum(true);
setAuthError(false);
console.log(authNumRef.current.value, email);
verify(authNumRef.current.value, email)
.then((res) => {
console.log(res);
setSentAuthNum(true);
})
.catch((err) => {
console.log(err);
if (err.message === 'Request failed with status code 404') {
setAuthError(true);
}
return;
});
};
const handlePost = async (e) => {
@ -25,7 +44,9 @@ export default function PasswordResetPage() {
if (!isPWMatch) {
return;
}
console.log(passwordRef.current.value, passwordConfirmRef.current.value);
updatePassword(passwordRef.current.value, email).then(() => {
navigate('/auth/login');
});
};
return sentAuthNum ? (
@ -48,9 +69,7 @@ export default function PasswordResetPage() {
ref={passwordConfirmRef}
hasError={!passwordMatch}
>
{!passwordMatch && (
<div className={`${styles.textBodyStrong} ${styles.dangerColor}`}>비밀번호가 일치하지 않습니다</div>
)}
{!passwordMatch && <div>비밀번호가 일치하지 않습니다</div>}
</InputBox>
</AuthForm>
</div>
@ -66,7 +85,9 @@ export default function PasswordResetPage() {
id="authNum"
type="password"
ref={authNumRef}
/>
>
{authError && <div>잘못된 인증번호입니다</div>}
</InputBox>
</AuthForm>
</div>
);

View File

@ -1,50 +1,53 @@
import { AuthForm, InputBox } from '../../components/AuthForm';
import { useRef, useState, useEffect } from 'react';
import styles from './PasswordResetPage.module.css';
import { Link, useNavigate } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { usePasswordReset } from '../../hooks/api/usePasswordReset';
export default function PasswordResetPage() {
const navigate = useNavigate();
const [time, setTime] = useState(5);
const [sendEmail, setSendEmail] = useState(false);
const [emailSent, setEmailSent] = useState('');
const [notFound, setNotFound] = useState(false);
const emailRef = useRef('');
const buttonText = useRef('비밀번호 찾기');
const handleSubmit = (e) => {
const { sendEmail } = usePasswordReset();
useEffect(() => {
if (emailSent) {
console.log('Updated emailSent:', emailSent);
}
}, [emailSent]);
const handleSubmit = async (e) => {
e.preventDefault();
console.log('비밀번호 찾기', emailRef.current.value);
setSendEmail(true);
setNotFound(false);
await sendEmail(emailRef.current.value)
.then(() => {
const email = emailRef.current.value;
console.log(email);
setEmailSent(email);
console.log(emailSent);
})
.catch((err) => {
console.log(err);
if (err.message === 'Request failed with status code 404') {
setNotFound(true);
}
return;
});
};
useEffect(() => {
if (!sendEmail) {
return;
}
const timer = setInterval(() => {
setTime((prev) => prev - 1);
}, 1000);
return () => clearInterval(timer);
}, [sendEmail]);
useEffect(() => {
if (time === 0) {
navigate('../resetAuth');
}
}, [navigate, time]);
return sendEmail ? (
return emailSent ? (
<section className={styles.loginGroup}>
<h1 className={styles.title}>비밀번호 </h1>
<h1 className={styles.title}>인증번호 받기</h1>
<p className={styles.text}>
비밀번호 초기화 인증번호를 이메일로 보냈습니다.
<br />
메일함을 확인해주세요.
<br />
<span className={styles.seconds}>{time}</span> 후에 자동으로 인증번호 입력 페이지로 이동합니다.
</p>
<Link
to={'../resetAuth'}
className={styles.linkButton}
state={emailSent}
>
인증번호 입력하러 가기
</Link>
@ -53,15 +56,18 @@ export default function PasswordResetPage() {
<div className={styles.wrapper}>
<AuthForm
onSubmit={handleSubmit}
title="비밀번호 찾기"
buttonText={buttonText.current}
title="비밀번호 재설정"
buttonText="인증번호 받기"
>
<InputBox
title="이메일"
id="email"
type="email"
ref={emailRef}
/>
hasError={notFound}
>
{notFound && <div>존재하지 않는 이메일입니다</div>}
</InputBox>
</AuthForm>
</div>
);

View File

@ -14,19 +14,16 @@ export default function UserRegisterPage() {
const passwordConfirmRef = useRef();
const [userType, setUserType] = useState('STUDENT');
const [passwordMatch, setPasswordMatch] = useState(true);
const [existingId, setExistingId] = useState(false);
const [existingEmail, setExistingEmail] = useState(false);
const [error, setError] = useState('');
const { userRegister } = useAuth();
const handleSubmit = async (e) => {
e.preventDefault();
const isPWMatch = passwordRef.current.value === passwordConfirmRef.current.value;
setExistingId(false);
setExistingEmail(false);
setPasswordMatch(isPWMatch);
setError('');
if (!isPWMatch) {
setError('pwNotMatch');
return;
}
userRegister(
@ -41,10 +38,10 @@ export default function UserRegisterPage() {
})
.catch((err) => {
if (err.response.data === '아이디가 중복 됐습니다.') {
setExistingId(true);
setError('existingId');
}
if (err.response.data === '이메일이 중복 됐습니다.') {
setExistingEmail(true);
setError('existingEmail');
}
});
};
@ -84,11 +81,9 @@ export default function UserRegisterPage() {
type="text"
id="ID"
ref={idRef}
hasError={existingId}
hasError={error === 'existingId'}
>
{existingId && (
<div className={`${styles.textBodyStrong} ${styles.dangerColor}`}>이미 존재하는 아이디입니다</div>
)}
{error === 'existingId' && <div>이미 존재하는 아이디입니다</div>}
</InputBox>
<InputBox
title="이름"
@ -101,11 +96,9 @@ export default function UserRegisterPage() {
type="email"
id="email"
ref={emailRef}
hasError={existingEmail}
hasError={error === 'existingEmail'}
>
{existingEmail && (
<div className={`${styles.textBodyStrong} ${styles.dangerColor}`}>이미 등록된 이메일입니다</div>
)}
{error === 'existingEmail' && <div>이미 등록된 이메일입니다</div>}
</InputBox>
<InputBox
title="비밀번호"
@ -118,11 +111,9 @@ export default function UserRegisterPage() {
type="password"
id="passwordConfirm"
ref={passwordConfirmRef}
hasError={!passwordMatch}
hasError={error === 'pwNotMatch'}
>
{!passwordMatch && (
<div className={`${styles.textBodyStrong} ${styles.dangerColor}`}>비밀번호가 일치하지 않습니다</div>
)}
{error === 'pwNotMatch' && <div>비밀번호가 일치하지 않습니다</div>}
</InputBox>
</AuthForm>
</div>