Merge branch 'FE/QuizsetListPage' into 'frontend'

[Front-End] feat: quizsetListPage 추가 외

See merge request s11-webmobile1-sub2/S11P12A701!19
This commit is contained in:
조현수 2024-08-02 12:08:21 +09:00
commit 3540180742
20 changed files with 513 additions and 8 deletions

View File

@ -26,6 +26,9 @@ const PasswordChangePage = lazy(async () => await import('./pages/PasswordChange
const LearningLecturesPage = lazy(async () => await import('./pages/LearningLecturesPage'));
const LectureCreatePage = lazy(async () => await import('./pages/LectureCreatePage'));
const LectureEditPage = lazy(async () => await import('./pages/LectureEditPage'));
const QuizsetListPage = lazy(async () => await import('./pages/QuizsetListPage'));
const QuizsetWritePage = lazy(async () => await import('./pages/QuizsetWritePage'));
const QuizsetDetailPage = lazy(async () => await import('./pages/QuizsetDetailPage'));
const router = createBrowserRouter([
{
@ -114,6 +117,23 @@ const router = createBrowserRouter([
},
],
},
{
path: 'quiz',
children: [
{
index: true,
element: <QuizsetListPage />,
},
{
path: 'write',
element: <QuizsetWritePage />,
},
{
path: ':quizsetId',
element: <QuizsetDetailPage />,
},
],
},
],
},
{

View File

@ -1,7 +1,16 @@
import { Link } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import styles from './Header.module.css';
import useBoundStore from '../../store';
import { useAuth } from '../../hooks/api/useAuth';
export default function Header() {
const navigate = useNavigate();
const userType = useBoundStore((state) => state.userType);
const { logout } = useAuth();
const handleClick = () => {
logout().then(navigate('/'));
};
return (
<header className={styles.header}>
<nav className={styles.nav}>
@ -28,12 +37,21 @@ export default function Header() {
</li>
</ul>
<ul className={styles.group}>
<li>
<Link to={'user/my'}>마이페이지</Link>
</li>
<li>
<Link to={'/auth/login'}>로그인</Link>
</li>
{userType && (
<>
<li>
<Link to={'user/my'}>마이페이지</Link>
</li>
<li>
<Link onClick={handleClick}>로그아웃</Link>
</li>
</>
)}
{!userType && (
<li>
<Link to={'/auth/login'}>로그인</Link>
</li>
)}
</ul>
</nav>
</header>

View File

@ -0,0 +1,86 @@
import { useState } from 'react';
import styles from './QuizCard.module.css';
export default function QuizCard({ quiz, index, updateQuiz }) {
const [question, setQuestion] = useState(quiz.question || '');
const [answer, setAnswer] = useState(quiz.answer || '');
const [choices, setChoices] = useState(quiz.choices || []);
const handleChoiceChange = (num, content) => {
const updatedChoices = choices.map((choice) => (choice.num === num ? { ...choice, content } : choice));
setChoices(updatedChoices);
updateQuiz(index, { question, answer, choices: updatedChoices });
};
const handleAddChoice = () => {
if (choices.length < 4) {
const newChoice = { num: choices.length + 1, content: '' };
const updatedChoices = [...choices, newChoice];
setChoices(updatedChoices);
updateQuiz(index, { question, answer, choices: updatedChoices });
}
};
const handlePopChoice = () => {
if (choices.length > 0) {
const updatedChoices = choices.slice(0, -1);
setChoices(updatedChoices);
updateQuiz(index, { question, answer, choices: updatedChoices });
}
};
return (
<div className={styles.card}>
<label>질문</label>
<input
type="text"
value={question}
onChange={(e) => {
setQuestion(e.target.value);
updateQuiz(index, { question: e.target.value, answer, choices });
}}
placeholder="질문 내용을 입력하세요"
/>
<label>정답</label>
<input
type="text"
value={answer}
onChange={(e) => {
setAnswer(e.target.value);
updateQuiz(index, { question, answer: e.target.value, choices });
}}
placeholder="정답을 입력하세요"
/>
<div>
<span>Tip: 선택지를 넣지 않는다면 단답형 문제가 됩니다</span>
</div>
<div className={styles.buttonsWrapper}>
<button
type="button"
onClick={handleAddChoice}
className={styles.button}
>
선택지 추가하기
</button>
<button
type="button"
onClick={handlePopChoice}
className={styles.removeButton}
>
선택지 줄이기
</button>
</div>
{choices.map?.((choice, idx) => (
<div key={idx}>
<label>선택지 {choice.num} : </label>
<input
type="text"
value={choice.content}
onChange={(e) => handleChoiceChange(choice.num, e.target.value)}
placeholder={`Choice ${choice.num}`}
/>
</div>
))}
</div>
);
}

View File

@ -0,0 +1,47 @@
.card {
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 10px 20px;
width: 400px;
display: flex;
flex-direction: column;
gap: 8px;
}
.buttonsWrapper {
display: flex;
flex-direction: row;
gap: 8px;
}
.removeButton {
display: flex;
align-items: center;
padding: 12px 16px;
border: 1px solid var(--accent-color);
background-color: var(--accent-color);
color: var(--on-primary);
stroke: var(--on-primary);
font-size: 16px;
line-height: 1.4;
font-weight: 700;
align-self: end;
border-radius: 8px;
cursor: pointer;
}
.button {
display: flex;
align-items: center;
padding: 12px 16px;
border: 1px solid var(--primary-color);
background-color: var(--primary-color);
color: var(--on-primary);
stroke: var(--on-primary);
font-size: 16px;
line-height: 1.4;
font-weight: 700;
align-self: end;
border-radius: 8px;
cursor: pointer;
}

View File

@ -0,0 +1,81 @@
import { useState } from 'react';
import QuizCard from './QuizCard';
import styles from './QuizsetForm.module.css';
import EditIcon from '/src/assets/icons/edit.svg?react';
import BackIcon from '/src/assets/icons/back.svg?react';
import { Link } from 'react-router-dom';
export default function QuizsetForm({ headerTitle, topic, to, onSubmit }) {
// TODO:
const [title, setTitle] = useState('');
const [quizzes, setQuizzes] = useState([]);
const [imageFile, setImageFile] = useState(null);
const handleAddQuiz = () => {
setQuizzes([...quizzes, { question: '', answer: '', choices: [] }]);
};
const updateQuiz = (index, updatedQuiz) => {
const updatedQuizzes = quizzes.map((quiz, i) => (i === index ? updatedQuiz : quiz));
setQuizzes(updatedQuizzes);
};
const handleFileChange = (e) => {
const file = e.target.files?.[0];
setImageFile(file);
};
return (
<div className={styles.quizsetForm}>
<header className={styles.header}>
<Link
to={to}
className={styles.goBack}
>
<BackIcon />
<span>{headerTitle}</span>
</Link>
<div className={styles.title}>{topic}</div>
</header>
<form
className={styles.form}
onSubmit={(e) => onSubmit(e, title, quizzes, imageFile)}
>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="퀴즈셋 제목을 입력해주세요"
/>
{quizzes.map((quiz, index) => (
<QuizCard
key={index}
quiz={quiz}
index={index}
updateQuiz={updateQuiz}
/>
))}
<button
type="button"
onClick={handleAddQuiz}
className={styles.button}
>
퀴즈 추가하기
</button>
<label>퀴즈 이미지</label>
<input
type="file"
accept=".png, .jpg, .jpeg"
onChange={handleFileChange}
/>
<button
type="submit"
className={styles.button}
>
<EditIcon />
<div>제출</div>
</button>
</form>
</div>
);
}

View File

@ -0,0 +1,74 @@
.quizsetForm {
background: var(--background-color);
width: 100%;
max-width: 900px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 40px;
}
.header {
display: flex;
flex-direction: column;
align-items: start;
gap: 8px;
}
.goBack {
display: flex;
align-items: center;
gap: 4px;
font-size: 20px;
line-height: 1.2;
font-weight: 400;
color: var(--text-color-secondary);
stroke: var(--text-color-secondary);
}
.title {
font-size: 32px;
line-height: 1.2;
font-weight: 800;
}
.form {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
gap: 20px;
}
.removeButton {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border: 1px solid var(--accent-color);
background-color: var(--accent-color);
color: var(--on-primary);
stroke: var(--on-primary);
font-size: 16px;
line-height: 1.4;
font-weight: 700;
align-self: end;
border-radius: 8px;
cursor: pointer;
}
.button {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border: 1px solid var(--primary-color);
background-color: var(--primary-color);
color: var(--on-primary);
stroke: var(--on-primary);
font-size: 16px;
line-height: 1.4;
font-weight: 700;
align-self: end;
border-radius: 8px;
cursor: pointer;
}

View File

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

View File

@ -0,0 +1,22 @@
import BackIcon from '/src/assets/icons/back.svg?react';
import { Link } from 'react-router-dom';
import styles from './QuizsetDetail.module.css';
export default function QuizsetDetail({ topic, title }) {
return (
<div className={styles.quizsetDetail}>
<header className={styles.header}>
<Link
to={'..'}
className={styles.goBack}
>
<BackIcon />
<span>{topic}</span>
</Link>
<div>
<h1 className={styles.title}>{title}</h1>
</div>
</header>
</div>
);
}

View File

@ -0,0 +1,36 @@
.quizsetDetail {
display: flex;
flex-direction: column;
gap: 20px;
width: 100%;
background-color: var(--background-default);
color: var(--text-color);
box-sizing: border-box;
margin: 0;
padding: 0;
}
.header {
display: flex;
flex-direction: column;
align-items: start;
gap: 8px;
}
.goBack {
display: flex;
align-items: center;
gap: 4px;
font-size: 20px;
line-height: 1.2;
font-weight: 400;
color: var(--text-color-secondary);
stroke: var(--text-color-secondary);
}
.title {
font-size: 32px;
line-height: 1.2;
font-weight: 800;
margin: 0;
}

View File

@ -0,0 +1 @@
export { default as QuizsetDetail } from './QuizsetDetail';

View File

@ -44,5 +44,16 @@ export function useAuth() {
});
};
return { login, userRegister };
const logout = () => {
return instance
.get(`${API_URL}/user/logout`)
.then((response) => {
console.log(response);
setUserType(null);
setToken(null);
})
.catch((e) => console.log(e));
};
return { login, logout, userRegister };
}

View File

@ -0,0 +1,10 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import instance from '../../utils/axios/instance';
import { API_URL } from '../../constants';
export function useQuizsetDetail(id) {
return useSuspenseQuery({
queryKey: ['quizset', id],
queryFn: () => instance.get(`${API_URL}/quiz/${id}`),
});
}

View File

@ -0,0 +1,14 @@
import instance from '../../utils/axios/instance';
import { API_URL } from '../../constants';
export function useQuizsetWrite() {
const quizsetWrite = (formData) => {
return instance.post(`${API_URL}/quiz`, formData, {
headers: {
'Content-type': 'multipart/form-data',
},
});
};
return { quizsetWrite };
}

View File

@ -0,0 +1,10 @@
import { useQuery } from '@tanstack/react-query';
import instance from '../../utils/axios/instance';
import { API_URL } from '../../constants';
export function useQuizsets() {
return useQuery({
queryKey: ['quizsetList'],
queryFn: () => instance.get(`${API_URL}/quiz`),
});
}

View File

@ -0,0 +1,11 @@
import { useQuizsetDetail } from '../../hooks/api/useQuizsetDetail';
import { useParams } from 'react-router-dom';
import { QuizsetDetail } from '../../components/QuizsetDetail';
export default function QuizsetListPage() {
const { lectureId } = useParams();
const { data } = useQuizsetDetail(lectureId);
const quizset = data?.data ?? [];
console.log(quizset);
return <QuizsetDetail title={quizset.title} />;
}

View File

@ -0,0 +1 @@
export { default } from './QuizsetDetailPage';

View File

@ -0,0 +1,27 @@
import { ArticleLink } from '../../components/ArticleLink';
import ArticleBoard from '../../components/ArticleBoard/ArticleBoard';
import { useQuizsets } from '../../hooks/api/useQuizsets';
import { useParams } from 'react-router-dom';
// import useBoundStore from '../../store';
export default function QuizsetListPage() {
const { lectureId } = useParams();
const { data } = useQuizsets(lectureId);
const quizsets = data?.data ?? [];
// const userType = useBoundStore((state) => state.userType);
console.log(quizsets);
return (
<ArticleBoard
title="퀴즈 목록"
canCreate={true}
>
{quizsets.map?.((quizset) => (
<ArticleLink
key={`${quizset.quizSetId}`}
title={quizset.title}
to={`${quizset.quizSetId}`}
/>
))}
</ArticleBoard>
);
}

View File

@ -0,0 +1 @@
export { default } from './QuizsetListPage';

View File

@ -0,0 +1,32 @@
import { QuizsetForm } from '../../components/QuizForm';
import { useQuizsetWrite } from '../../hooks/api/useQuizsetWrite';
export default function QuizsetWritePage() {
// TODO: lecture
const { quizsetWrite } = useQuizsetWrite();
const handleSubmit = async (e, title, quizzes, imageFile = null) => {
e.preventDefault();
const quizsetObject = {
title,
quizzes,
};
console.log(quizsetObject);
console.log(imageFile);
const formData = new FormData();
formData.append('quizSetCreateRequest', new Blob([JSON.stringify(quizsetObject)], { type: 'application/json' }));
if (imageFile) {
formData.append('image', imageFile);
}
const response = await quizsetWrite(formData);
console.log(response);
};
return (
<QuizsetForm
onSubmit={handleSubmit}
headerTitle={'퀴즈 목록'}
topic={'퀴즈 작성'}
to={'..'}
/>
);
}

View File

@ -0,0 +1 @@
export { default } from './QuizsetWritePage';