Merge branch 'frontend' into 'fe/useAcceptLecture'
# Conflicts: # frontend/src/Router.jsx
This commit is contained in:
commit
30e159273e
@ -5,9 +5,9 @@ import HomePage from './pages/HomePage';
|
||||
import NotFoundPage from './pages/NotFoundPage';
|
||||
import { lazy } from 'react';
|
||||
import MyPageLayout from './components/Layout/MyPageLayout';
|
||||
import { LiveLayout } from './components/Layout';
|
||||
import LivePage from './pages/LivePage';
|
||||
import ErrorPage from './pages/ErrorPage';
|
||||
|
||||
const LivePage = lazy(async () => await import('./pages/LivePage'));
|
||||
const LectureLayout = lazy(async () => await import('./components/Layout/LectureLayout'));
|
||||
const LearningLectureDetailPage = lazy(async () => await import('./pages/LearningLectureDetailPage'));
|
||||
const NoticeListPage = lazy(async () => await import('./pages/NoticeListPage'));
|
||||
@ -31,22 +31,21 @@ 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 LectureEnrollPage = lazy(async () => await import('./pages/LectureEnrollPage'));
|
||||
const QuizsetEditPage = lazy(async () => await import('./pages/QuizsetEditPage'));
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '*',
|
||||
element: <NotFoundPage />,
|
||||
},
|
||||
{
|
||||
path: 'live/:roomId',
|
||||
element: <LiveLayout />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <LivePage />,
|
||||
},
|
||||
],
|
||||
element: <LivePage />,
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
element: <PageLayout />,
|
||||
errorElement: <NotFoundPage />,
|
||||
errorElement: <ErrorPage />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
@ -145,7 +144,16 @@ const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: ':quizsetId',
|
||||
element: <QuizsetDetailPage />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <QuizsetDetailPage />,
|
||||
},
|
||||
{
|
||||
path: 'edit',
|
||||
element: <QuizsetEditPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -1,11 +1,15 @@
|
||||
import styles from './InfoEditForm.module.css';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function InfoEditForm() {
|
||||
export default function InfoEditForm({ onSubmit }) {
|
||||
const [username, setUsername] = useState('');
|
||||
const [useremail, setUseremail] = useState('');
|
||||
|
||||
return (
|
||||
<form className={styles.infoEditForm}>
|
||||
<form
|
||||
onSubmit={(e) => onSubmit(e, username, useremail)}
|
||||
className={styles.infoEditForm}
|
||||
>
|
||||
<p className={styles.textHeading}>이름 변경</p>
|
||||
<div className={styles.inputBox}>
|
||||
<label
|
||||
|
@ -4,7 +4,7 @@ import styles from './LectureForm.module.css';
|
||||
import EditIcon from '/src/assets/icons/edit.svg?react';
|
||||
import BackIcon from '/src/assets/icons/back.svg?react';
|
||||
|
||||
export default function LectureForm({ title, topic, to, initialValues = {}, onSubmit, onCreate = false }) {
|
||||
export default function LectureForm({ title, topic, to = '..', initialValues = {}, onSubmit, onCreate = false }) {
|
||||
// TODO: 디자인 필요, 필요시 useState로 수정하고 버튼 비활성화 기능 추가
|
||||
const titleRef = useRef('');
|
||||
const descriptionRef = useRef('');
|
||||
|
@ -1,3 +1,4 @@
|
||||
import styles from './LiveRoom.module.css';
|
||||
import { isEqualTrackRef, isTrackReference } from '@livekit/components-core';
|
||||
import {
|
||||
CarouselLayout,
|
||||
@ -11,14 +12,22 @@ import {
|
||||
RoomAudioRenderer,
|
||||
useCreateLayoutContext,
|
||||
usePinnedTracks,
|
||||
useRoomInfo,
|
||||
useTracks,
|
||||
useParticipants,
|
||||
useLocalParticipant,
|
||||
} from '@livekit/components-react';
|
||||
import { RoomEvent, Track } from 'livekit-client';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import ChatRoom from '../ChatRoom/ChatRoom';
|
||||
|
||||
export default function LiveRoom() {
|
||||
const lastAutoFocusedScreenShareTrack = useRef(null);
|
||||
const [role, setRole] = useState(null);
|
||||
|
||||
const room = useRoomInfo();
|
||||
const participants = useParticipants();
|
||||
const { localParticipant } = useLocalParticipant();
|
||||
|
||||
const tracks = useTracks(
|
||||
[
|
||||
@ -36,6 +45,16 @@ export default function LiveRoom() {
|
||||
const focusTrack = usePinnedTracks(layoutContext)?.[0];
|
||||
const carouselTracks = tracks.filter((track) => !isEqualTrackRef(track, focusTrack));
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const role = JSON.parse(localParticipant.identity).role;
|
||||
|
||||
setRole(role);
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
}, [localParticipant.identity]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
screenShareTracks.some((track) => track.publication.isSubscribed) &&
|
||||
@ -69,31 +88,40 @@ export default function LiveRoom() {
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="lk-video-conference">
|
||||
<LayoutContextProvider value={layoutContext}>
|
||||
<div className="lk-video-conference-inner">
|
||||
{!focusTrack ? (
|
||||
<div className="lk-grid-layout-wrapper">
|
||||
<GridLayout tracks={tracks}>
|
||||
<ParticipantTile />
|
||||
</GridLayout>
|
||||
</div>
|
||||
) : (
|
||||
<div className="lk-focus-layout-wrapper">
|
||||
<FocusLayoutContainer>
|
||||
<CarouselLayout tracks={carouselTracks}>
|
||||
<ParticipantTile />
|
||||
</CarouselLayout>
|
||||
{focusTrack && <FocusLayout trackRef={focusTrack} />}
|
||||
</FocusLayoutContainer>
|
||||
</div>
|
||||
)}
|
||||
<ControlBar controls={{ chat: false, leave: false }} />
|
||||
<div className={styles.wrapper}>
|
||||
<header className={styles.header}>
|
||||
<h1 className={styles.title}>{room.name}</h1>
|
||||
<div className={styles.roomInfo}>
|
||||
<span>참가자</span>
|
||||
<span>{participants.length}명</span>
|
||||
</div>
|
||||
<ChatRoom />
|
||||
</LayoutContextProvider>
|
||||
<RoomAudioRenderer />
|
||||
<ConnectionStateToast />
|
||||
</header>
|
||||
<div className="lk-video-conference">
|
||||
<LayoutContextProvider value={layoutContext}>
|
||||
<div className="lk-video-conference-inner">
|
||||
{!focusTrack ? (
|
||||
<div className="lk-grid-layout-wrapper">
|
||||
<GridLayout tracks={tracks}>
|
||||
<ParticipantTile />
|
||||
</GridLayout>
|
||||
</div>
|
||||
) : (
|
||||
<div className="lk-focus-layout-wrapper">
|
||||
<FocusLayoutContainer>
|
||||
<CarouselLayout tracks={carouselTracks}>
|
||||
<ParticipantTile />
|
||||
</CarouselLayout>
|
||||
{focusTrack && <FocusLayout trackRef={focusTrack} />}
|
||||
</FocusLayoutContainer>
|
||||
</div>
|
||||
)}
|
||||
<ControlBar controls={{ chat: false, leave: false, screenShare: role === '강사' }} />
|
||||
</div>
|
||||
<ChatRoom />
|
||||
</LayoutContextProvider>
|
||||
<RoomAudioRenderer />
|
||||
<ConnectionStateToast />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,67 +1,32 @@
|
||||
.main {
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.videoWrapper {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
height: 80px;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
box-sizing: border-box;
|
||||
|
||||
& > audio {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--text-color-tertiary);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
& > video {
|
||||
width: 100%;
|
||||
height: calc(100vh - 208px);
|
||||
object-fit: contain;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.controlBar {
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 80px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
box-sizing: border-box;
|
||||
|
||||
& > button {
|
||||
background-color: var(--background-color-secondary);
|
||||
color: var(--text-color-primary);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-color-tertiary);
|
||||
}
|
||||
}
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 0 16px;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.roomInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0 16px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import styles from './PasswordChangeForm.module.css';
|
||||
|
||||
export default function PasswordChangeForm() {
|
||||
export default function PasswordChangeForm({ onSubmit, onPwError = false }) {
|
||||
// TODO: onPwError(현재 비밀번호와 같음) 시 응답을 받아 표시
|
||||
const [errorConfirmMessage, setErrorConfirmMessage] = useState(false);
|
||||
const [errorSameMessage, setErrorSameMessage] = useState(false);
|
||||
const currentPasswordRef = useRef('');
|
||||
const newPasswordRef = useRef('');
|
||||
const confirmPasswordRef = useRef('');
|
||||
const userPassword = '1234';
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
@ -15,13 +15,15 @@ export default function PasswordChangeForm() {
|
||||
const newPassword = newPasswordRef.current.value;
|
||||
const confirmPassword = confirmPasswordRef.current.value;
|
||||
|
||||
if (currentPassword === userPassword) {
|
||||
setErrorSameMessage(false);
|
||||
} else {
|
||||
setErrorSameMessage(true);
|
||||
}
|
||||
if (newPassword === confirmPassword) {
|
||||
setErrorConfirmMessage(false);
|
||||
onSubmit(currentPassword, newPassword, confirmPassword);
|
||||
|
||||
if (onPwError) {
|
||||
setErrorSameMessage(true);
|
||||
} else {
|
||||
setErrorSameMessage(false);
|
||||
}
|
||||
} else {
|
||||
setErrorConfirmMessage(true);
|
||||
}
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { useState } from 'react';
|
||||
import styles from './QuizCard.module.css';
|
||||
|
||||
export default function QuizCard({ quiz, index, updateQuiz }) {
|
||||
export default function QuizCard({ quiz, updateQuiz, deleteQuiz }) {
|
||||
const [question, setQuestion] = useState(quiz.question || '');
|
||||
const [answer, setAnswer] = useState(quiz.answer || '');
|
||||
const [choices, setChoices] = useState(quiz.choices || []);
|
||||
const [image, setImage] = useState(quiz.image || null);
|
||||
|
||||
const handleChoiceChange = (num, content) => {
|
||||
const updatedChoices = choices.map((choice) => (choice.num === num ? { ...choice, content } : choice));
|
||||
setChoices(updatedChoices);
|
||||
updateQuiz(index, { question, answer, choices: updatedChoices });
|
||||
updateQuiz(quiz.id, { ...quiz, question, answer, choices: updatedChoices, image });
|
||||
};
|
||||
|
||||
const handleAddChoice = () => {
|
||||
@ -17,7 +18,7 @@ export default function QuizCard({ quiz, index, updateQuiz }) {
|
||||
const newChoice = { num: choices.length + 1, content: '' };
|
||||
const updatedChoices = [...choices, newChoice];
|
||||
setChoices(updatedChoices);
|
||||
updateQuiz(index, { question, answer, choices: updatedChoices });
|
||||
updateQuiz(quiz.id, { ...quiz, question, answer, choices: updatedChoices, image });
|
||||
}
|
||||
};
|
||||
|
||||
@ -25,19 +26,29 @@ export default function QuizCard({ quiz, index, updateQuiz }) {
|
||||
if (choices.length > 0) {
|
||||
const updatedChoices = choices.slice(0, -1);
|
||||
setChoices(updatedChoices);
|
||||
updateQuiz(index, { question, answer, choices: updatedChoices });
|
||||
updateQuiz(quiz.id, { ...quiz, question, answer, choices: updatedChoices, image });
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
const file = e.target.files[0] ?? null;
|
||||
setImage(file);
|
||||
updateQuiz(quiz.id, { ...quiz, question, answer, choices, image: file });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.card}>
|
||||
<div className={styles.header}>
|
||||
<span>퀴즈 생성 카드</span>
|
||||
<span onClick={() => deleteQuiz(quiz.id)}>X</span> {/* id를 기반으로 삭제 */}
|
||||
</div>
|
||||
<label>질문</label>
|
||||
<input
|
||||
type="text"
|
||||
value={question}
|
||||
onChange={(e) => {
|
||||
setQuestion(e.target.value);
|
||||
updateQuiz(index, { question: e.target.value, answer, choices });
|
||||
updateQuiz(quiz.id, { ...quiz, question: e.target.value, answer, choices, image });
|
||||
}}
|
||||
placeholder="질문 내용을 입력하세요"
|
||||
/>
|
||||
@ -47,7 +58,7 @@ export default function QuizCard({ quiz, index, updateQuiz }) {
|
||||
value={answer}
|
||||
onChange={(e) => {
|
||||
setAnswer(e.target.value);
|
||||
updateQuiz(index, { question, answer: e.target.value, choices });
|
||||
updateQuiz(quiz.id, { ...quiz, question, answer: e.target.value, choices, image });
|
||||
}}
|
||||
placeholder="정답을 입력하세요"
|
||||
/>
|
||||
@ -81,6 +92,12 @@ export default function QuizCard({ quiz, index, updateQuiz }) {
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<label>퀴즈 이미지</label>
|
||||
<input
|
||||
type="file"
|
||||
accept=".png, .jpg, .jpeg"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -8,6 +8,12 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.buttonsWrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -1,28 +1,39 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } 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: 디자인 만들기 및 스타일 적용
|
||||
export default function QuizsetForm({ headerTitle, topic, to, onSubmit, initialValue = null }) {
|
||||
const [title, setTitle] = useState('');
|
||||
const [quizzes, setQuizzes] = useState([]);
|
||||
const [imageFile, setImageFile] = useState(null);
|
||||
const [quizId, setQuizId] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValue) {
|
||||
setTitle(initialValue.title || '');
|
||||
setQuizzes(initialValue.quizzes || []);
|
||||
setQuizId(initialValue.quizzes ? initialValue.quizzes[initialValue.quizzes.length - 1].id + 1 : 0);
|
||||
console.log(initialValue.quizzes.length);
|
||||
}
|
||||
}, [initialValue]);
|
||||
|
||||
const handleAddQuiz = () => {
|
||||
setQuizzes([...quizzes, { question: '', answer: '', choices: [] }]);
|
||||
console.log(quizzes);
|
||||
setQuizzes([...quizzes, { id: quizId, question: '', answer: '', choices: [], image: null }]);
|
||||
setQuizId(quizId + 1);
|
||||
};
|
||||
|
||||
const updateQuiz = (index, updatedQuiz) => {
|
||||
const updatedQuizzes = quizzes.map((quiz, i) => (i === index ? updatedQuiz : quiz));
|
||||
const updateQuiz = (id, updatedQuiz) => {
|
||||
console.log(quizzes);
|
||||
const updatedQuizzes = quizzes.map((quiz) => (quiz.id === id ? updatedQuiz : quiz));
|
||||
setQuizzes(updatedQuizzes);
|
||||
};
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
setImageFile(file);
|
||||
const deleteQuiz = (id) => {
|
||||
console.log(quizzes);
|
||||
setQuizzes(quizzes.filter((quiz) => quiz.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
@ -39,7 +50,7 @@ export default function QuizsetForm({ headerTitle, topic, to, onSubmit }) {
|
||||
</header>
|
||||
<form
|
||||
className={styles.form}
|
||||
onSubmit={(e) => onSubmit(e, title, quizzes, imageFile)}
|
||||
onSubmit={(e) => onSubmit(e, title, quizzes)}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
@ -47,12 +58,12 @@ export default function QuizsetForm({ headerTitle, topic, to, onSubmit }) {
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="퀴즈셋 제목을 입력해주세요"
|
||||
/>
|
||||
{quizzes.map((quiz, index) => (
|
||||
{quizzes.map((quiz) => (
|
||||
<QuizCard
|
||||
key={index}
|
||||
key={quiz.id}
|
||||
quiz={quiz}
|
||||
index={index}
|
||||
updateQuiz={updateQuiz}
|
||||
deleteQuiz={deleteQuiz}
|
||||
/>
|
||||
))}
|
||||
<button
|
||||
@ -62,12 +73,6 @@ export default function QuizsetForm({ headerTitle, topic, to, onSubmit }) {
|
||||
>
|
||||
퀴즈 추가하기
|
||||
</button>
|
||||
<label>퀴즈 이미지</label>
|
||||
<input
|
||||
type="file"
|
||||
accept=".png, .jpg, .jpeg"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className={styles.button}
|
||||
|
@ -2,7 +2,7 @@ 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 }) {
|
||||
export default function QuizsetDetail({ topic, title, quizzes = [], onDelete, onEdit }) {
|
||||
return (
|
||||
<div className={styles.quizsetDetail}>
|
||||
<header className={styles.header}>
|
||||
@ -17,6 +17,30 @@ export default function QuizsetDetail({ topic, title }) {
|
||||
<h1 className={styles.title}>{title}</h1>
|
||||
</div>
|
||||
</header>
|
||||
<div>
|
||||
{quizzes.map((quiz, index) => (
|
||||
<div key={index}>
|
||||
<div>질문 : {quiz.question}</div>
|
||||
<img
|
||||
src={quiz.image}
|
||||
alt="강의 이미지"
|
||||
/>
|
||||
<div>정답 : {quiz.answer}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDelete}
|
||||
>
|
||||
퀴즈셋 삭제
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onEdit}
|
||||
>
|
||||
퀴즈셋 수정
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -59,5 +59,23 @@ export function useAuth() {
|
||||
});
|
||||
};
|
||||
|
||||
return { login, logout, userRegister };
|
||||
const updateInfo = (name, email) => {
|
||||
const infoBody = {
|
||||
name,
|
||||
email,
|
||||
};
|
||||
return instance.put(`${API_URL}/user/updateinfo`, infoBody);
|
||||
};
|
||||
|
||||
const updatePassword = (currentPw, newPw, newPwCheck) => {
|
||||
const passwordBody = {
|
||||
currentPassword: currentPw,
|
||||
newPassword: newPw,
|
||||
newPasswordCheck: newPwCheck,
|
||||
};
|
||||
console.log(passwordBody);
|
||||
return instance.put(`${API_URL}/user/updatepassword`, passwordBody);
|
||||
};
|
||||
|
||||
return { login, logout, userRegister, updateInfo, updatePassword };
|
||||
}
|
||||
|
10
frontend/src/hooks/api/useQuizsetDelete.js
Normal file
10
frontend/src/hooks/api/useQuizsetDelete.js
Normal file
@ -0,0 +1,10 @@
|
||||
import instance from '../../utils/axios/instance';
|
||||
import { API_URL } from '../../constants';
|
||||
|
||||
export function useQuizsetDelete() {
|
||||
const quizsetDelete = (quizsetId) => {
|
||||
return instance.delete(`${API_URL}/quiz/teacher/${quizsetId}`);
|
||||
};
|
||||
|
||||
return { quizsetDelete };
|
||||
}
|
@ -5,6 +5,6 @@ import { API_URL } from '../../constants';
|
||||
export function useQuizsetDetail(id) {
|
||||
return useSuspenseQuery({
|
||||
queryKey: ['quizset', id],
|
||||
queryFn: () => instance.get(`${API_URL}/quiz/${id}`),
|
||||
queryFn: () => instance.get(`${API_URL}/quiz/teacher/${id}`),
|
||||
});
|
||||
}
|
||||
|
14
frontend/src/hooks/api/useQuizsetEdit.js
Normal file
14
frontend/src/hooks/api/useQuizsetEdit.js
Normal file
@ -0,0 +1,14 @@
|
||||
import instance from '../../utils/axios/instance';
|
||||
import { API_URL } from '../../constants';
|
||||
|
||||
export function useQuizsetEdit() {
|
||||
const quizsetEdit = (formData) => {
|
||||
return instance.put(`${API_URL}/quiz`, formData, {
|
||||
headers: {
|
||||
'Content-type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return { quizsetEdit };
|
||||
}
|
45
frontend/src/pages/ErrorPage/ErrorPage.jsx
Normal file
45
frontend/src/pages/ErrorPage/ErrorPage.jsx
Normal file
@ -0,0 +1,45 @@
|
||||
import styles from './ErrorPage.module.css';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Header } from '../../components/Header';
|
||||
import { Footer } from '../../components/Footer';
|
||||
|
||||
export default function ErrorPage() {
|
||||
const [time, setTime] = useState(5);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setTime((prev) => prev - 1);
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (time === 0) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [navigate, time]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.contents}>
|
||||
<p className={styles.title}>에러가 발생했습니다.</p>
|
||||
<p className={styles.msg}>
|
||||
<span className={styles.seconds}>{time}초</span> 후에 자동으로 홈으로 이동합니다.
|
||||
</p>
|
||||
<Link
|
||||
to={'/'}
|
||||
className={styles.link}
|
||||
>
|
||||
홈으로 가기
|
||||
</Link>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
49
frontend/src/pages/ErrorPage/ErrorPage.module.css
Normal file
49
frontend/src/pages/ErrorPage/ErrorPage.module.css
Normal file
@ -0,0 +1,49 @@
|
||||
.wrapper {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
padding-top: 64px;
|
||||
color: var(--text-color);
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.contents {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 32px;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.msg {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.seconds {
|
||||
color: var(--primary-color);
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: var(--primary-color);
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
font-weight: 700;
|
||||
}
|
1
frontend/src/pages/ErrorPage/index.js
Normal file
1
frontend/src/pages/ErrorPage/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './ErrorPage';
|
@ -21,9 +21,10 @@ export default function LectureCreatePage() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>강의 생성</h1>
|
||||
<LectureForm
|
||||
title={'강의 생성'}
|
||||
title={'강의 홈'}
|
||||
topic={'강의 생성'}
|
||||
to={'..'}
|
||||
onSubmit={handleSubmit}
|
||||
onCreate={true}
|
||||
/>
|
||||
|
@ -6,6 +6,7 @@ import instance from '../../utils/axios/instance';
|
||||
import { API_URL, ROOM_URL } from '../../constants';
|
||||
import useBoundStore from '../../store';
|
||||
import '@livekit/components-styles';
|
||||
import LoadingIndicator from '../../components/LoadingIndicator.jsx/LoadingIndicator';
|
||||
|
||||
export default function LivePage() {
|
||||
const { roomId } = useParams();
|
||||
@ -26,16 +27,16 @@ export default function LivePage() {
|
||||
}
|
||||
}, [generateToken, liveToken]);
|
||||
|
||||
return (
|
||||
liveToken && (
|
||||
<LiveKitRoom
|
||||
token={liveToken}
|
||||
serverUrl={ROOM_URL}
|
||||
connect={true}
|
||||
data-lk-theme="default"
|
||||
>
|
||||
<LiveRoom />
|
||||
</LiveKitRoom>
|
||||
)
|
||||
return liveToken ? (
|
||||
<LiveKitRoom
|
||||
token={liveToken}
|
||||
serverUrl={ROOM_URL}
|
||||
connect={true}
|
||||
data-lk-theme="default"
|
||||
>
|
||||
<LiveRoom />
|
||||
</LiveKitRoom>
|
||||
) : (
|
||||
<LoadingIndicator fill />
|
||||
);
|
||||
}
|
||||
|
@ -1,11 +1,30 @@
|
||||
import { useQuizsetDetail } from '../../hooks/api/useQuizsetDetail';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { QuizsetDetail } from '../../components/QuizsetDetail';
|
||||
import { useQuizsetDelete } from '../../hooks/api/useQuizsetDelete';
|
||||
|
||||
export default function QuizsetListPage() {
|
||||
const { lectureId } = useParams();
|
||||
const { data } = useQuizsetDetail(lectureId);
|
||||
const quizset = data?.data ?? [];
|
||||
export default function QuizsetDetailPage() {
|
||||
const navigate = useNavigate();
|
||||
const { quizsetId } = useParams();
|
||||
const { quizsetDelete } = useQuizsetDelete();
|
||||
const { data } = useQuizsetDetail(quizsetId);
|
||||
const quizset = data.data;
|
||||
console.log(quizset);
|
||||
return <QuizsetDetail title={quizset.title} />;
|
||||
const handleEdit = () => {
|
||||
navigate('edit', { state: { initialValue: quizset } });
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
await quizsetDelete(quizsetId);
|
||||
navigate('..');
|
||||
};
|
||||
return (
|
||||
<QuizsetDetail
|
||||
topic={'퀴즈 목록'}
|
||||
title={quizset.title}
|
||||
quizzes={quizset.quizzes}
|
||||
onDelete={handleDelete}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
59
frontend/src/pages/QuizsetEditPage/QuizsetEditPage.jsx
Normal file
59
frontend/src/pages/QuizsetEditPage/QuizsetEditPage.jsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { QuizsetForm } from '../../components/QuizForm';
|
||||
import { useQuizsetEdit } from '../../hooks/api/useQuizsetEdit';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
export default function QuizsetEditPage() {
|
||||
const { quizsetId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const initialValue = location.state.initialValue;
|
||||
const { quizsetEdit } = useQuizsetEdit();
|
||||
|
||||
const handleSubmit = async (e, title, quizzes) => {
|
||||
e.preventDefault();
|
||||
console.log(quizzes);
|
||||
|
||||
const images = [];
|
||||
const quizContents = [];
|
||||
quizzes.forEach((quiz) => {
|
||||
const { image, ...quizData } = quiz;
|
||||
images.push(image);
|
||||
quizContents.push(quizData);
|
||||
});
|
||||
|
||||
const quizsetObject = {
|
||||
id: quizsetId,
|
||||
title,
|
||||
quizzes: quizContents,
|
||||
};
|
||||
console.log(quizsetObject);
|
||||
const formData = new FormData();
|
||||
formData.append('quizSetUpdateRequest', new Blob([JSON.stringify(quizsetObject)], { type: 'application/json' }));
|
||||
|
||||
images.forEach((imageFile) => {
|
||||
if (imageFile && !(typeof imageFile === 'string')) {
|
||||
formData.append('images', imageFile);
|
||||
} else {
|
||||
formData.append('images', new Blob([''], { type: 'image/jpg' }));
|
||||
}
|
||||
});
|
||||
|
||||
formData.forEach((value, key) => {
|
||||
console.log(`FormData - Key: ${key}, Value:`, value);
|
||||
});
|
||||
|
||||
await quizsetEdit(formData);
|
||||
navigate('..');
|
||||
};
|
||||
|
||||
return (
|
||||
<QuizsetForm
|
||||
initialValue={initialValue}
|
||||
onSubmit={handleSubmit}
|
||||
headerTitle={initialValue.title}
|
||||
topic={'퀴즈 수정'}
|
||||
to={'..'}
|
||||
/>
|
||||
);
|
||||
}
|
1
frontend/src/pages/QuizsetEditPage/index.js
Normal file
1
frontend/src/pages/QuizsetEditPage/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './QuizsetEditPage';
|
@ -1,26 +1,44 @@
|
||||
import { QuizsetForm } from '../../components/QuizForm';
|
||||
import { useQuizsetWrite } from '../../hooks/api/useQuizsetWrite';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export default function QuizsetWritePage() {
|
||||
// TODO: lecture에서 이미지 전송 성공 후 해당 방법으로 이미지 파일 입력
|
||||
const navigate = useNavigate();
|
||||
const { quizsetWrite } = useQuizsetWrite();
|
||||
const handleSubmit = async (e, title, quizzes, imageFile = null) => {
|
||||
|
||||
const handleSubmit = async (e, title, quizzes) => {
|
||||
e.preventDefault();
|
||||
console.log(quizzes);
|
||||
|
||||
const images = [];
|
||||
const quizContents = [];
|
||||
|
||||
quizzes.forEach((quiz) => {
|
||||
const { image, ...quizData } = quiz;
|
||||
images.push(image);
|
||||
quizContents.push(quizData);
|
||||
});
|
||||
|
||||
const quizsetObject = {
|
||||
title,
|
||||
quizzes,
|
||||
quizzes: quizContents,
|
||||
};
|
||||
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);
|
||||
|
||||
images.forEach((imageFile) => {
|
||||
if (imageFile) {
|
||||
formData.append('images', imageFile);
|
||||
} else {
|
||||
formData.append('images', new Blob([''], { type: 'image/jpg' }));
|
||||
}
|
||||
});
|
||||
|
||||
await quizsetWrite(formData);
|
||||
navigate('..');
|
||||
};
|
||||
|
||||
return (
|
||||
<QuizsetForm
|
||||
onSubmit={handleSubmit}
|
||||
|
@ -1,4 +1,6 @@
|
||||
export const liveSlice = (set) => ({
|
||||
liveToken: null,
|
||||
setLiveToken: (liveToken) => set({ liveToken }),
|
||||
participants: 0,
|
||||
setParticipants: (participants) => set({ participants }),
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user