feat: 퀴즈 생성 카드에 토글 추가
This commit is contained in:
parent
72aa60cefb
commit
bcdb8cb31b
@ -1,17 +1,23 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import styles from './QuizCard.module.css';
|
||||
import CloseIcon from '/src/assets/icons/close.svg?react';
|
||||
import PlusIcon from '/src/assets/icons/plus.svg?react';
|
||||
import { Toggle } from '../Toggle';
|
||||
import { STATIC_URL } from '../../constants';
|
||||
|
||||
export default function QuizCard({ quiz, updateQuiz, deleteQuiz }) {
|
||||
// TODO: 카드 디자인 완성 및 이쁘게 바꾸기
|
||||
const [question, setQuestion] = useState(quiz.question || '');
|
||||
const [answer, setAnswer] = useState(Number(quiz.answer) || '');
|
||||
const [choices, setChoices] = useState(quiz.choices || []);
|
||||
const [choices, setChoices] = useState(quiz.choices || [{ num: 1, content: '' }]);
|
||||
const [image, setImage] = useState(quiz.image || null);
|
||||
const [imagePreview, setImagePreview] = useState(
|
||||
quiz.image ? `${import.meta.env.VITE_STATIC_URL}${quiz.image}` : null
|
||||
);
|
||||
const [imagePreview, setImagePreview] = useState(quiz.image ? `${STATIC_URL}${quiz.image}` : null);
|
||||
const [quizType, setQuizType] = useState('단답식');
|
||||
|
||||
const clearImage = () => {
|
||||
setImage(null);
|
||||
setImagePreview(null);
|
||||
};
|
||||
|
||||
const handleChoiceChange = (num, content) => {
|
||||
const updatedChoices = choices.map((choice) => (choice.num === num ? { ...choice, content } : choice));
|
||||
setChoices(updatedChoices);
|
||||
@ -19,32 +25,36 @@ export default function QuizCard({ quiz, updateQuiz, deleteQuiz }) {
|
||||
};
|
||||
|
||||
const handleAddChoice = () => {
|
||||
if (choices.length < 4) {
|
||||
if (choices.length >= 4) {
|
||||
return;
|
||||
}
|
||||
const newChoice = { num: choices.length + 1, content: '' };
|
||||
const updatedChoices = [...choices, newChoice];
|
||||
setChoices(updatedChoices);
|
||||
updateQuiz(quiz.id, { ...quiz, question, answer, choices: updatedChoices, image });
|
||||
}
|
||||
};
|
||||
|
||||
const handlePopChoice = () => {
|
||||
if (choices.length > 0) {
|
||||
if (choices.length <= 1) {
|
||||
return;
|
||||
}
|
||||
const updatedChoices = choices.slice(0, -1);
|
||||
setChoices(updatedChoices);
|
||||
if (updatedChoices.length < answer) {
|
||||
setAnswer('');
|
||||
setAnswer(1);
|
||||
}
|
||||
updateQuiz(quiz.id, { ...quiz, question, answer, choices: updatedChoices, image });
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
const file = e.target.files[0] ?? null;
|
||||
if (!file || !file.type.startsWith('image/')) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
if (!file.type.startsWith('image/')) {
|
||||
alert('이미지 파일만 업로드 해주세요');
|
||||
e.target.value = null;
|
||||
setImage(null);
|
||||
setImagePreview(null);
|
||||
clearImage();
|
||||
return;
|
||||
}
|
||||
setImage(file);
|
||||
@ -61,15 +71,31 @@ export default function QuizCard({ quiz, updateQuiz, deleteQuiz }) {
|
||||
};
|
||||
|
||||
const handleChoiceSelect = (choiceContent) => {
|
||||
console.log(choiceContent);
|
||||
setAnswer(choiceContent);
|
||||
updateQuiz(quiz.id, { ...quiz, question, answer: choiceContent, choices, image });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
quizType === '단답식' ? setAnswer('') : setAnswer(1);
|
||||
}, [quizType]);
|
||||
|
||||
return (
|
||||
<div className={styles.card}>
|
||||
<div className={styles.header}>
|
||||
<span className={styles.heading}>퀴즈 생성 카드</span>
|
||||
<div className={styles.titleGroup}>
|
||||
<span>Q.</span>
|
||||
<input
|
||||
type="text"
|
||||
value={question}
|
||||
maxLength={200}
|
||||
autoFocus
|
||||
onChange={(e) => {
|
||||
setQuestion(e.target.value);
|
||||
updateQuiz(quiz.id, { ...quiz, question: e.target.value, answer, choices, image });
|
||||
}}
|
||||
placeholder="질문 내용을 입력하세요"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className={`${styles.cardRemove}`}
|
||||
onClick={() => deleteQuiz(quiz.id)}
|
||||
@ -77,20 +103,37 @@ export default function QuizCard({ quiz, updateQuiz, deleteQuiz }) {
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
<label htmlFor={`file-input-${quiz.id}`}>
|
||||
<div className={styles.content}>
|
||||
{imagePreview ? (
|
||||
<div className={styles.imageArea}>
|
||||
<label htmlFor={`file-input-${quiz.id}`}>
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="Preview"
|
||||
className={styles.imagePreview}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearImage}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<label htmlFor={`file-input-${quiz.id}`}>
|
||||
<div className={styles.imagePreview}>
|
||||
<PlusIcon />
|
||||
<span>퀴즈 이미지 추가</span>
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
<div className={styles.answerArea}>
|
||||
<Toggle
|
||||
active={quizType}
|
||||
setActive={setQuizType}
|
||||
choices={['단답식', '객관식']}
|
||||
/>
|
||||
<input
|
||||
id={`file-input-${quiz.id}`}
|
||||
type="file"
|
||||
@ -98,32 +141,49 @@ export default function QuizCard({ quiz, updateQuiz, deleteQuiz }) {
|
||||
onChange={handleFileChange}
|
||||
className={styles.hiddenInput}
|
||||
/>
|
||||
<label className={styles.label}>질문</label>
|
||||
<input
|
||||
type="text"
|
||||
value={question}
|
||||
maxLength={200}
|
||||
onChange={(e) => {
|
||||
setQuestion(e.target.value);
|
||||
updateQuiz(quiz.id, { ...quiz, question: e.target.value, answer, choices, image });
|
||||
}}
|
||||
className={styles.input}
|
||||
placeholder="질문 내용을 입력하세요"
|
||||
/>
|
||||
<label className={styles.label}>정답</label>
|
||||
{choices.length > 0 ? (
|
||||
<div className={styles.choicesWrapper}>
|
||||
<div className={styles.label}>정답</div>
|
||||
{quizType === '객관식' ? (
|
||||
<>
|
||||
{choices.map((choice, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={styles.choice}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
key={index + 1}
|
||||
onClick={() => handleChoiceSelect(index + 1)}
|
||||
className={`${styles.choiceButton} ${answer === index + 1 ? styles.selected : ''}`}
|
||||
>
|
||||
{index + 1}
|
||||
</button>
|
||||
))}
|
||||
<input
|
||||
className={`${styles.input} ${styles.choiceInput}`}
|
||||
type="text"
|
||||
maxLength={200}
|
||||
value={choice.content}
|
||||
onChange={(e) => handleChoiceChange(choice.num, e.target.value)}
|
||||
placeholder={`보기 ${choice.num}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className={styles.buttonsWrapper}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePopChoice}
|
||||
className={`${styles.button} ${styles.remove} ${choices.length <= 1 ? styles.hidden : ''}`}
|
||||
>
|
||||
보기 줄이기
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddChoice}
|
||||
className={`${styles.button} ${styles.add} ${choices.length >= 4 ? styles.hidden : ''}`}
|
||||
>
|
||||
보기 추가
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
@ -137,43 +197,9 @@ export default function QuizCard({ quiz, updateQuiz, deleteQuiz }) {
|
||||
placeholder="정답을 입력하세요"
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<span>Tip: 선택지를 넣지 않는다면 단답형 문제가 됩니다</span>
|
||||
</div>
|
||||
<div className={styles.buttonsWrapper}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddChoice}
|
||||
className={`${styles.button} ${styles.add}`}
|
||||
>
|
||||
선택지 추가하기
|
||||
</button>
|
||||
{choices.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePopChoice}
|
||||
className={`${styles.button} ${styles.remove}`}
|
||||
>
|
||||
선택지 줄이기
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{choices.map?.((choice, idx) => (
|
||||
<div
|
||||
className={styles.choiceDiv}
|
||||
key={idx}
|
||||
>
|
||||
<label>선택지 {choice.num} </label>
|
||||
<input
|
||||
className={`${styles.input} ${styles.choiceInput}`}
|
||||
type="text"
|
||||
maxLength={200}
|
||||
value={choice.content}
|
||||
onChange={(e) => handleChoiceChange(choice.num, e.target.value)}
|
||||
placeholder={`Choice ${choice.num}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,31 +1,81 @@
|
||||
.card {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 16px 12px;
|
||||
width: 416px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
.titleGroup {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--text-color);
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
|
||||
& > input {
|
||||
flex-grow: 1;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-color-tertiary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--text-color);
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.imageArea {
|
||||
position: relative;
|
||||
|
||||
& > button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
padding: 4px;
|
||||
background-color: transparent;
|
||||
stroke: var(--error-color);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.1s,
|
||||
stroke 0.1s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--error-color);
|
||||
stroke: var(--on-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.imagePreview {
|
||||
@ -37,7 +87,6 @@
|
||||
align-items: center;
|
||||
width: 295px;
|
||||
height: 220px;
|
||||
margin: 10px auto;
|
||||
border-radius: 8px;
|
||||
background-color: var(--background-secondary);
|
||||
object-fit: contain;
|
||||
@ -49,8 +98,15 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.answerArea {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 14px;
|
||||
padding: 12px;
|
||||
background: var(--background);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
@ -69,7 +125,7 @@
|
||||
|
||||
.choiceInput {
|
||||
flex-grow: 1;
|
||||
padding: 7px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
@ -77,6 +133,7 @@
|
||||
}
|
||||
|
||||
.buttonsWrapper {
|
||||
align-self: end;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
@ -85,27 +142,28 @@
|
||||
.button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
padding: 8px 12px;
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
font-weight: 700;
|
||||
font-weight: 500;
|
||||
align-self: end;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add {
|
||||
border: 1px solid var(--primary-color);
|
||||
border: none;
|
||||
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);
|
||||
/* border: 1px solid var(--blue100); */
|
||||
border: none;
|
||||
background-color: var(--background-tertiary);
|
||||
color: var(--text-color-);
|
||||
stroke: var(--text-color-);
|
||||
}
|
||||
|
||||
.cardRemove {
|
||||
@ -119,22 +177,27 @@
|
||||
|
||||
.choicesWrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.choice {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.choiceButton {
|
||||
padding: 10px 16px;
|
||||
padding: 12px 16px;
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--background-tertiary);
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.25s,
|
||||
border-color 0.25s,
|
||||
stroke 0.25s,
|
||||
color 0.25s;
|
||||
background-color 0.1s,
|
||||
border-color 0.1s,
|
||||
stroke 0.1s,
|
||||
color 0.1s;
|
||||
}
|
||||
|
||||
.choiceButton:hover {
|
||||
@ -152,3 +215,7 @@
|
||||
border: 1px solid var(--primary-color);
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ export default function QuizsetForm({ headerTitle, topic, to, onSubmit, initialV
|
||||
}, [initialValue]);
|
||||
|
||||
const handleAddQuiz = () => {
|
||||
setQuizzes([...quizzes, { id: quizId, question: '', answer: '', choices: [], image: null }]);
|
||||
setQuizzes([...quizzes, { id: quizId, question: '', answer: '', choices: [{ num: 1, content: '' }], image: null }]);
|
||||
setQuizId(quizId + 1);
|
||||
};
|
||||
|
||||
@ -49,7 +49,7 @@ export default function QuizsetForm({ headerTitle, topic, to, onSubmit, initialV
|
||||
className={styles.form}
|
||||
onSubmit={(e) => onSubmit(e, title, quizzes)}
|
||||
>
|
||||
<label className={styles.label}>퀴즈셋 제목</label>
|
||||
<label className={styles.label}>제목</label>
|
||||
<input
|
||||
className={styles.input}
|
||||
type="text"
|
||||
@ -58,7 +58,7 @@ export default function QuizsetForm({ headerTitle, topic, to, onSubmit, initialV
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="퀴즈셋 제목을 입력해주세요"
|
||||
/>
|
||||
<div className={styles.grid}>
|
||||
<div className={styles.quizList}>
|
||||
{quizzes.map((quiz) => (
|
||||
<QuizCard
|
||||
key={quiz.id}
|
||||
|
@ -63,18 +63,20 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 440px);
|
||||
.quizList {
|
||||
/* display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 440px); */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
justify-content: start;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.addCard {
|
||||
width: 440px;
|
||||
/* width: 440px; */
|
||||
cursor: pointer;
|
||||
height: 592px;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@ -83,6 +85,7 @@
|
||||
stroke: var(--text-color);
|
||||
border-radius: 8px;
|
||||
gap: 10px;
|
||||
transition: background-color 0.1s;
|
||||
}
|
||||
|
||||
.addCard:hover {
|
||||
|
18
frontend/src/components/Toggle/Toggle.jsx
Normal file
18
frontend/src/components/Toggle/Toggle.jsx
Normal file
@ -0,0 +1,18 @@
|
||||
import styles from './Toggle.module.css';
|
||||
|
||||
export default function Toggle({ choices, active, setActive }) {
|
||||
return (
|
||||
<div className={styles.toggle}>
|
||||
{choices?.map((choice) => (
|
||||
<button
|
||||
key={choice}
|
||||
type="button"
|
||||
className={choice === active ? styles.active : ''}
|
||||
onClick={() => setActive(choice)}
|
||||
>
|
||||
{choice}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
28
frontend/src/components/Toggle/Toggle.module.css
Normal file
28
frontend/src/components/Toggle/Toggle.module.css
Normal file
@ -0,0 +1,28 @@
|
||||
.toggle {
|
||||
display: flex;
|
||||
justify-content: stretch;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--background);
|
||||
|
||||
& > button {
|
||||
border: none;
|
||||
background-color: var(--background);
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
color: var(--text-color);
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
color: var(--on-primary);
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
}
|
1
frontend/src/components/Toggle/index.js
Normal file
1
frontend/src/components/Toggle/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default as Toggle } from './Toggle';
|
@ -112,7 +112,7 @@
|
||||
--whiteOpacity900: #fff;
|
||||
|
||||
/* semantic colors */
|
||||
--primary-color: #05f;
|
||||
--primary-color: #36f;
|
||||
--accent-color: #f50;
|
||||
|
||||
--background: #fff;
|
||||
|
Loading…
Reference in New Issue
Block a user