Merge branch 'frontend' into 'fe/freeboard'
# Conflicts: # frontend/src/Router.jsx
This commit is contained in:
commit
acb47badbe
@ -36,6 +36,7 @@ const FreeboardListPage = lazy(async () => await import('./pages/FreeboardListPa
|
||||
const CreateFreeboardPage = lazy(async () => await import('./pages/CreateFreeboardPage'));
|
||||
const FreeboardDetailPage = lazy(async () => await import('./pages/FreeboardDetailPage'));
|
||||
const EditFreeboardPage = lazy(async () => await import('./pages/EditFreeboardPage'));
|
||||
const PasswordResetAuthPage = lazy(async () => await import('./pages/PasswordResetAuthPage'));
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@ -200,6 +201,10 @@ const router = createBrowserRouter([
|
||||
path: 'password-reset',
|
||||
element: <PasswordResetPage />,
|
||||
},
|
||||
{
|
||||
path: 'resetAuth',
|
||||
element: <PasswordResetAuthPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
5
frontend/src/assets/icons/close.svg
Normal file
5
frontend/src/assets/icons/close.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="X">
|
||||
<path id="Icon" d="M24 8.5L8 24.5M8 8.5L24 24.5" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 234 B |
@ -1,11 +1,23 @@
|
||||
import styles from './ChatRoom.module.css';
|
||||
import SendIcon from '/src/assets/icons/send.svg?react';
|
||||
import { cloneElement, useEffect, useRef } from 'react';
|
||||
import { cloneElement, Suspense, useEffect, useRef, useState } from 'react';
|
||||
import { ChatEntry, useChat, useMaybeLayoutContext } from '@livekit/components-react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import useChatRoom from '../../hooks/chat/useChatRoom';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { QuizModal } from '../QuizModal';
|
||||
import { QuizSet } from '../QuizSet';
|
||||
|
||||
export default function ChatRoom({ ...props }) {
|
||||
export default function ChatRoom({ isTeacher, ...props }) {
|
||||
const { roomId } = useParams();
|
||||
const wsChat = useChatRoom(roomId);
|
||||
const chatInputRef = useRef(null);
|
||||
const ulRef = useRef(null);
|
||||
const [isQuizModalOpen, setIsQuizModalOpen] = useState(false);
|
||||
|
||||
const openQuizModal = () => {
|
||||
setIsQuizModalOpen(true);
|
||||
};
|
||||
|
||||
const { send, chatMessages, isSending } = useChat();
|
||||
|
||||
@ -54,59 +66,94 @@ export default function ChatRoom({ ...props }) {
|
||||
}, [chatMessages, layoutContext, layoutContext.widget]);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className="lk-chat"
|
||||
>
|
||||
<h2 className={styles.title}>채팅</h2>
|
||||
|
||||
<ul
|
||||
className="lk-list lk-chat-messages"
|
||||
ref={ulRef}
|
||||
<>
|
||||
<div
|
||||
{...props}
|
||||
className={`lk-chat ${wsChat.quizSetId ? styles.none : ''}`}
|
||||
>
|
||||
{props.children
|
||||
? chatMessages.map((msg, idx) =>
|
||||
cloneElement(props.children, {
|
||||
entry: msg,
|
||||
key: msg.id ?? idx,
|
||||
})
|
||||
)
|
||||
: chatMessages.map((msg, idx, allMsg) => {
|
||||
const hideName = idx >= 1 && allMsg[idx - 1].from === msg.from;
|
||||
const hideTimestamp = idx >= 1 && msg.timestamp - allMsg[idx - 1].timestamp < 60_000;
|
||||
<header className={styles.header}>
|
||||
<h2 className={styles.title}>채팅</h2>
|
||||
{isTeacher && (
|
||||
<button
|
||||
className={styles.quizButton}
|
||||
onClick={openQuizModal}
|
||||
>
|
||||
퀴즈 시작
|
||||
</button>
|
||||
)}
|
||||
</header>
|
||||
|
||||
return (
|
||||
<ChatEntry
|
||||
key={msg.id ?? idx}
|
||||
hideName={hideName}
|
||||
hideTimestamp={hideName === false ? false : hideTimestamp}
|
||||
entry={msg}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<form
|
||||
className="lk-chat-form"
|
||||
onSubmit={handleChatSubmit}
|
||||
>
|
||||
<input
|
||||
className="lk-form-control lk-chat-form-input"
|
||||
disabled={isSending}
|
||||
ref={chatInputRef}
|
||||
type="text"
|
||||
placeholder="메시지"
|
||||
onInput={(ev) => ev.stopPropagation()}
|
||||
onKeyDown={(ev) => ev.stopPropagation()}
|
||||
onKeyUp={(ev) => ev.stopPropagation()}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className={`lk-button lk-chat-form-button ${styles.button}`}
|
||||
disabled={isSending}
|
||||
<ul
|
||||
className="lk-list lk-chat-messages"
|
||||
ref={ulRef}
|
||||
>
|
||||
<SendIcon />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{props.children
|
||||
? chatMessages.map((msg, idx) =>
|
||||
cloneElement(props.children, {
|
||||
entry: msg,
|
||||
key: msg.id ?? idx,
|
||||
})
|
||||
)
|
||||
: chatMessages.map((msg, idx, allMsg) => {
|
||||
const hideName = idx >= 1 && allMsg[idx - 1].from === msg.from;
|
||||
const hideTimestamp = idx >= 1 && msg.timestamp - allMsg[idx - 1].timestamp < 60_000;
|
||||
|
||||
return (
|
||||
<ChatEntry
|
||||
key={msg.id ?? idx}
|
||||
hideName={hideName}
|
||||
hideTimestamp={hideName === false ? false : hideTimestamp}
|
||||
entry={msg}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<form
|
||||
className="lk-chat-form"
|
||||
onSubmit={handleChatSubmit}
|
||||
>
|
||||
<input
|
||||
className="lk-form-control lk-chat-form-input"
|
||||
disabled={isSending}
|
||||
ref={chatInputRef}
|
||||
type="text"
|
||||
placeholder="메시지"
|
||||
onInput={(ev) => ev.stopPropagation()}
|
||||
onKeyDown={(ev) => ev.stopPropagation()}
|
||||
onKeyUp={(ev) => ev.stopPropagation()}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className={`lk-button lk-chat-form-button ${styles.button}`}
|
||||
disabled={isSending}
|
||||
>
|
||||
<SendIcon />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{wsChat.quizSetId && (
|
||||
<div className="lk-chat">
|
||||
<header className={styles.header}>
|
||||
<h2 className={styles.title}>퀴즈</h2>
|
||||
</header>
|
||||
<Suspense fallback={<></>}>
|
||||
<QuizSet
|
||||
quizSetId={wsChat.quizSetId}
|
||||
finish={() => wsChat.setQuizSetId(null)}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isQuizModalOpen &&
|
||||
createPortal(
|
||||
<QuizModal
|
||||
startQuiz={wsChat.startQuiz}
|
||||
quizSets={wsChat.quizSets}
|
||||
closeModal={() => setIsQuizModalOpen(false)}
|
||||
/>,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -8,9 +8,39 @@
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
|
||||
& > .quizButton {
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
font-weight: 700;
|
||||
border: 1px solid var(--border-color-tertiary);
|
||||
background-color: #1e1e1e;
|
||||
transition:
|
||||
background-color 0.2s,
|
||||
scale 0.1s;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: #2b2b2b;
|
||||
}
|
||||
|
||||
&:active {
|
||||
scale: 0.95;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.messageList {
|
||||
@ -133,3 +163,7 @@
|
||||
.button {
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
.none {
|
||||
display: none !important;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import styles from './ClassCard.module.css';
|
||||
import CompassIcon from '/src/assets/icons/compass.svg?react';
|
||||
import { STATIC_URL } from '../../constants';
|
||||
|
||||
export default function ClassCard({ img, path, children }) {
|
||||
return (
|
||||
@ -10,7 +11,7 @@ export default function ClassCard({ img, path, children }) {
|
||||
>
|
||||
{img ? (
|
||||
<img
|
||||
src={`${import.meta.env.VITE_STATIC_URL}${img}`}
|
||||
src={`${STATIC_URL}${img}`}
|
||||
alt="강의 이미지"
|
||||
className={styles.thumbnail}
|
||||
/>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import styles from './ClassInfo.module.css';
|
||||
|
||||
export default function ClassInfo({ classTerm, classTime, onSubmit }) {
|
||||
export default function ClassInfo({ classTerm, classTime, status = 'NOT_ENROLLED', onSubmit }) {
|
||||
// TODO: 수강신청 취소(필요시) 기능구현
|
||||
return (
|
||||
<div className={styles.classInfo}>
|
||||
<div className={styles.title}>수업정보</div>
|
||||
@ -17,8 +18,11 @@ export default function ClassInfo({ classTerm, classTime, onSubmit }) {
|
||||
<button
|
||||
onClick={onSubmit}
|
||||
className={styles.button}
|
||||
disabled={status === 'PENDING'}
|
||||
>
|
||||
수강신청
|
||||
{status === 'PENDING' && '수강신청 중'}
|
||||
{status === 'ENROLLED' && '강의 상세페이지로 이동'}
|
||||
{status === 'NOT_ENROLLED' && '수강신청'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
@ -55,3 +55,11 @@
|
||||
color: var(--on-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
border-color: var(--border-color);
|
||||
background-color: var(--background-tertiary);
|
||||
color: var(--text-color-tertiary);
|
||||
cursor: not-allowed;
|
||||
stroke: var(--text-color-tertiary);
|
||||
}
|
||||
|
@ -1,9 +1,14 @@
|
||||
import styles from './InfoEditForm.module.css';
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function InfoEditForm({ onSubmit }) {
|
||||
const [username, setUsername] = useState('');
|
||||
const [useremail, setUseremail] = useState('');
|
||||
export default function InfoEditForm({ name, email, onSubmit }) {
|
||||
const [username, setUsername] = useState(name);
|
||||
const [useremail, setUseremail] = useState(email);
|
||||
|
||||
useEffect(() => {
|
||||
setUsername(name);
|
||||
setUseremail(email);
|
||||
}, [name, email]);
|
||||
|
||||
return (
|
||||
<form
|
||||
@ -46,7 +51,12 @@ export default function InfoEditForm({ onSubmit }) {
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button className={styles.buttonBox}>내 정보 수정</button>
|
||||
<button
|
||||
disabled={(!username && !useremail) || (username == name && useremail == email)}
|
||||
className={styles.buttonBox}
|
||||
>
|
||||
내 정보 수정
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
@ -55,3 +55,12 @@
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.buttonBox:disabled,
|
||||
.buttonBox[disabled] {
|
||||
border-color: var(--border-color);
|
||||
background-color: var(--background-tertiary);
|
||||
color: var(--text-color-tertiary);
|
||||
cursor: not-allowed;
|
||||
stroke: var(--text-color-tertiary);
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ export default function LectureLayout() {
|
||||
const { lectureDelete } = useLectureDelete();
|
||||
const { data } = useLectureInfo(lectureId);
|
||||
const lecture = data?.data;
|
||||
console.log(lecture);
|
||||
const userType = useBoundStore((state) => state.userType);
|
||||
const handleDelete = async () => {
|
||||
await lectureDelete(lectureId);
|
||||
@ -36,8 +37,7 @@ export default function LectureLayout() {
|
||||
title={lecture.title}
|
||||
tutor={lecture.teacherName}
|
||||
img={lecture.image}
|
||||
// TODO: isLive를 받아올 수단 추가
|
||||
isLive={true}
|
||||
isLive={lecture.online}
|
||||
/>
|
||||
<MaxWidthLayout hasSideBar>
|
||||
<aside>
|
||||
|
@ -120,7 +120,7 @@ export default function LiveRoom() {
|
||||
controls={{ chat: false, leave: true, screenShare: role === '강사' }}
|
||||
/>
|
||||
</div>
|
||||
<ChatRoom />
|
||||
<ChatRoom isTeacher={role === '강사'} />
|
||||
</LayoutContextProvider>
|
||||
<RoomAudioRenderer />
|
||||
<ConnectionStateToast />
|
||||
|
@ -1,14 +1,17 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import styles from './PasswordChangeForm.module.css';
|
||||
|
||||
export default function PasswordChangeForm({ onSubmit, onPwError = false }) {
|
||||
// TODO: onPwError(현재 비밀번호와 같음) 시 응답을 받아 표시
|
||||
export default function PasswordChangeForm({ onSubmit, pwError = false }) {
|
||||
const [errorConfirmMessage, setErrorConfirmMessage] = useState(false);
|
||||
const [errorSameMessage, setErrorSameMessage] = useState(false);
|
||||
const currentPasswordRef = useRef('');
|
||||
const newPasswordRef = useRef('');
|
||||
const confirmPasswordRef = useRef('');
|
||||
|
||||
useEffect(() => {
|
||||
setErrorSameMessage(pwError);
|
||||
}, [pwError]);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const currentPassword = currentPasswordRef.current.value;
|
||||
@ -18,12 +21,6 @@ export default function PasswordChangeForm({ onSubmit, onPwError = false }) {
|
||||
if (newPassword === confirmPassword) {
|
||||
setErrorConfirmMessage(false);
|
||||
onSubmit(currentPassword, newPassword, confirmPassword);
|
||||
|
||||
if (onPwError) {
|
||||
setErrorSameMessage(true);
|
||||
} else {
|
||||
setErrorSameMessage(false);
|
||||
}
|
||||
} else {
|
||||
setErrorConfirmMessage(true);
|
||||
}
|
||||
|
49
frontend/src/components/Quiz/Quiz.jsx
Normal file
49
frontend/src/components/Quiz/Quiz.jsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { useState } from 'react';
|
||||
import { STATIC_URL } from '../../constants';
|
||||
import styles from './Quiz.module.css';
|
||||
|
||||
export default function Quiz({ question, step, image, choices = [], setAnswers }) {
|
||||
const [answer, setAnswer] = useState(null);
|
||||
const isChoice = choices.length > 0;
|
||||
|
||||
return (
|
||||
<div className={styles.quiz}>
|
||||
<header className={styles.header}>
|
||||
<h1>{question}</h1>
|
||||
{image && (
|
||||
<img
|
||||
src={`${STATIC_URL}${image}`}
|
||||
alt="문제 이미지"
|
||||
/>
|
||||
)}
|
||||
</header>
|
||||
<div className={styles.choiceWrapper}>
|
||||
{isChoice ? (
|
||||
choices.map(({ id, num, content }) => (
|
||||
<div
|
||||
key={id}
|
||||
onClick={() => {
|
||||
setAnswer(num);
|
||||
setAnswers(num);
|
||||
}}
|
||||
className={`${styles.choice} ${answer === num ? styles.active : ''}`}
|
||||
>
|
||||
<span className={styles.number}>{num}</span>
|
||||
<span>{content}</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
placeholder="답 입력"
|
||||
onChange={(e) =>
|
||||
setAnswers((prev) => prev.map((value, index) => (index === step ? e.target.value : value)))
|
||||
}
|
||||
className={styles.input}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
78
frontend/src/components/Quiz/Quiz.module.css
Normal file
78
frontend/src/components/Quiz/Quiz.module.css
Normal file
@ -0,0 +1,78 @@
|
||||
@keyframes show {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.quiz {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
animation: show 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
|
||||
& > h1 {
|
||||
font-size: 24px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
& > img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 200px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.choiceWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
animation: show 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.5s forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.choice {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color-tertiary);
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
|
||||
& > .number {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color-tertiary);
|
||||
}
|
1
frontend/src/components/Quiz/index.js
Normal file
1
frontend/src/components/Quiz/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default as Quiz } from './Quiz';
|
@ -7,8 +7,9 @@ export default function QuizCard({ quiz, updateQuiz, deleteQuiz }) {
|
||||
const [answer, setAnswer] = useState(quiz.answer || '');
|
||||
const [choices, setChoices] = useState(quiz.choices || []);
|
||||
const [image, setImage] = useState(quiz.image || null);
|
||||
const [imagePreview, setImagePreview] = useState(quiz.image || null);
|
||||
|
||||
const [imagePreview, setImagePreview] = useState(
|
||||
quiz.image ? `${import.meta.env.VITE_STATIC_URL}${quiz.image}` : null
|
||||
);
|
||||
const handleChoiceChange = (num, content) => {
|
||||
const updatedChoices = choices.map((choice) => (choice.num === num ? { ...choice, content } : choice));
|
||||
setChoices(updatedChoices);
|
||||
@ -36,7 +37,6 @@ export default function QuizCard({ quiz, updateQuiz, deleteQuiz }) {
|
||||
const file = e.target.files[0] ?? null;
|
||||
setImage(file);
|
||||
updateQuiz(quiz.id, { ...quiz, question, answer, choices, image: file });
|
||||
|
||||
if (file) {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onloadend = () => {
|
||||
@ -51,22 +51,34 @@ export default function QuizCard({ quiz, updateQuiz, deleteQuiz }) {
|
||||
return (
|
||||
<div className={styles.card}>
|
||||
<div className={styles.header}>
|
||||
<span>퀴즈 생성 카드</span>
|
||||
<button onClick={() => deleteQuiz(quiz.id)}>X</button>
|
||||
<span className={styles.heading}>퀴즈 생성 카드</span>
|
||||
<button
|
||||
className={`${styles.button} ${styles.cardRemove}`}
|
||||
onClick={() => deleteQuiz(quiz.id)}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
<label>퀴즈 이미지</label>
|
||||
<label htmlFor={`file-input-${quiz.id}`}>
|
||||
{imagePreview ? (
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="Preview"
|
||||
className={styles.imagePreview}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.imagePreview}>
|
||||
<div>이미지 업로드</div>
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
id={`file-input-${quiz.id}`}
|
||||
type="file"
|
||||
accept=".png, .jpg, .jpeg"
|
||||
onChange={handleFileChange}
|
||||
className={styles.hiddenInput}
|
||||
/>
|
||||
{imagePreview && (
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="Preview"
|
||||
className={styles.imagePreview}
|
||||
/>
|
||||
)}
|
||||
<label className={styles.label}>질문</label>
|
||||
<input
|
||||
type="text"
|
||||
@ -109,9 +121,13 @@ export default function QuizCard({ quiz, updateQuiz, deleteQuiz }) {
|
||||
</button>
|
||||
</div>
|
||||
{choices.map?.((choice, idx) => (
|
||||
<div key={idx}>
|
||||
<label>선택지 {choice.num} : </label>
|
||||
<div
|
||||
className={styles.choiceDiv}
|
||||
key={idx}
|
||||
>
|
||||
<label>선택지 {choice.num} </label>
|
||||
<input
|
||||
className={`${styles.input} ${styles.choiceInput}`}
|
||||
type="text"
|
||||
value={choice.content}
|
||||
onChange={(e) => handleChoiceChange(choice.num, e.target.value)}
|
||||
|
@ -2,7 +2,7 @@
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 16px 12px;
|
||||
width: 400px;
|
||||
width: 416px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
@ -14,6 +14,12 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--text-color);
|
||||
font-size: 14px;
|
||||
@ -23,11 +29,19 @@
|
||||
}
|
||||
|
||||
.imagePreview {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 10px 0;
|
||||
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 {
|
||||
@ -40,6 +54,19 @@
|
||||
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);
|
||||
}
|
||||
@ -75,3 +102,10 @@
|
||||
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);
|
||||
}
|
||||
|
@ -69,19 +69,19 @@ export default function QuizsetForm({ headerTitle, topic, to, onSubmit, initialV
|
||||
deleteQuiz={deleteQuiz}
|
||||
/>
|
||||
))}
|
||||
<button
|
||||
<div
|
||||
type="button"
|
||||
onClick={handleAddQuiz}
|
||||
className={`${styles.button} ${styles.add}`}
|
||||
className={styles.addCard}
|
||||
>
|
||||
퀴즈 추가하기
|
||||
</button>
|
||||
카드 추가
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className={styles.createButton}
|
||||
className={`${styles.button} ${styles.add} ${styles.create}`}
|
||||
>
|
||||
<EditIcon className={styles.edit} />
|
||||
<EditIcon />
|
||||
<div>퀴즈 생성하기</div>
|
||||
</button>
|
||||
</form>
|
||||
|
@ -64,17 +64,29 @@
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 440px);
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
border: 1px solid var(--border-color);
|
||||
justify-content: start;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.addCard {
|
||||
width: 440px;
|
||||
cursor: pointer;
|
||||
height: 592px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--background-secondary);
|
||||
stroke: var(--text-color);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
font-size: 16px;
|
||||
@ -91,21 +103,8 @@
|
||||
stroke: var(--on-primary);
|
||||
}
|
||||
|
||||
.createButton {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
border: 1px solid var(--accent-color);
|
||||
background-color: var(--accent-color);
|
||||
color: var(--on-primary);
|
||||
stroke: var(--on-primary);
|
||||
.create {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
.edit {
|
||||
|
45
frontend/src/components/QuizModal/QuizModal.jsx
Normal file
45
frontend/src/components/QuizModal/QuizModal.jsx
Normal file
@ -0,0 +1,45 @@
|
||||
import styles from './QuizModal.module.css';
|
||||
import { useRef } from 'react';
|
||||
import CloseIcon from '/src/assets/icons/close.svg?react';
|
||||
|
||||
export default function QuizModal({ startQuiz, quizSets, closeModal }) {
|
||||
const wrapperRef = useRef(null);
|
||||
|
||||
const handleClick = (e) => {
|
||||
if (e.target === wrapperRef.current) {
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.wrapper}
|
||||
onClick={handleClick}
|
||||
ref={wrapperRef}
|
||||
>
|
||||
<div className={styles.modal}>
|
||||
<header className={styles.title}>
|
||||
<div>퀴즈 선택</div>
|
||||
<button onClick={closeModal}>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</header>
|
||||
<ul className={styles.list}>
|
||||
{quizSets.map((quizSet) => (
|
||||
<li
|
||||
key={quizSet.quizSetId}
|
||||
onClick={() => {
|
||||
console.log(quizSet.quizSetId);
|
||||
startQuiz(quizSet.quizSetId);
|
||||
closeModal();
|
||||
}}
|
||||
className={styles.quiz}
|
||||
>
|
||||
{quizSet.title}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
94
frontend/src/components/QuizModal/QuizModal.module.css
Normal file
94
frontend/src/components/QuizModal/QuizModal.module.css
Normal file
@ -0,0 +1,94 @@
|
||||
@keyframes show {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 100px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(12px);
|
||||
z-index: 1000;
|
||||
animation: show 0.25s;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
padding: 16px;
|
||||
color: var(--background-secondary);
|
||||
background-color: #111;
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow);
|
||||
animation: slideUp 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
stroke: var(--background);
|
||||
|
||||
& > button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.quiz {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background-color: var(--text-color);
|
||||
color: var(--on-primary);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.25s;
|
||||
}
|
1
frontend/src/components/QuizModal/index.js
Normal file
1
frontend/src/components/QuizModal/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default as QuizModal } from './QuizModal';
|
92
frontend/src/components/QuizSet/QuizSet.jsx
Normal file
92
frontend/src/components/QuizSet/QuizSet.jsx
Normal file
@ -0,0 +1,92 @@
|
||||
import styles from './QuizSet.module.css';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import Quiz from '../Quiz/Quiz';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import LoadingIndicator from '../LoadingIndicator.jsx/LoadingIndicator';
|
||||
import instance from '../../utils/axios/instance';
|
||||
import { API_URL } from '../../constants';
|
||||
import { useStudentQuizsetDetail } from '../../hooks/api/useStudentQuizsetDetail';
|
||||
|
||||
export default function QuizSet({ quizSetId, finish }) {
|
||||
const { roomId } = useParams();
|
||||
const [step, setStep] = useState(null);
|
||||
const { data } = useStudentQuizsetDetail(quizSetId);
|
||||
const quizSetData = data?.data;
|
||||
const quizList = quizSetData.quizzes;
|
||||
const answers = useRef(Array(quizList.length).fill(null));
|
||||
const interval = useRef(null);
|
||||
const submit = useCallback(
|
||||
(data) => {
|
||||
instance.post(`${API_URL}/report/submit/${roomId}/quizset/${quizSetId}`, data).catch(() => {});
|
||||
},
|
||||
[quizSetId, roomId]
|
||||
);
|
||||
const QuizComponents = [
|
||||
...quizList.map((quiz, index) => (
|
||||
<Quiz
|
||||
key={index}
|
||||
step={index}
|
||||
answers={answers.current}
|
||||
setAnswers={(value) => {
|
||||
answers.current = answers.current.map((v, i) => (i === index ? value : v));
|
||||
}}
|
||||
{...quiz}
|
||||
/>
|
||||
)),
|
||||
<div
|
||||
key={Infinity}
|
||||
className={styles.message}
|
||||
>
|
||||
퀴즈 종료
|
||||
</div>,
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (!quizList) {
|
||||
return;
|
||||
}
|
||||
|
||||
interval.current = setInterval(() => {
|
||||
setStep((prev) => {
|
||||
if (prev === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (prev + 1 === quizList.length) {
|
||||
submit(answers.current);
|
||||
} else if (prev === quizList.length) {
|
||||
return Infinity;
|
||||
}
|
||||
|
||||
return prev + 1;
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval.current);
|
||||
interval.current = null;
|
||||
};
|
||||
}, [quizList, submit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (step === Infinity) {
|
||||
finish();
|
||||
}
|
||||
}, [finish, step]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{step === null ? (
|
||||
<div className={styles.message}>
|
||||
<span>퀴즈를 시작합니다</span>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{QuizComponents[step]}
|
||||
<div className={styles.progressBar} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
44
frontend/src/components/QuizSet/QuizSet.module.css
Normal file
44
frontend/src/components/QuizSet/QuizSet.module.css
Normal file
@ -0,0 +1,44 @@
|
||||
@keyframes show {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 24px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
animation: show 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
@keyframes progress {
|
||||
from {
|
||||
transform: scaleX(0);
|
||||
}
|
||||
to {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background-color: var(--primary-color);
|
||||
overflow: hidden;
|
||||
margin-top: 16px;
|
||||
transform-origin: left center;
|
||||
animation: progress 5s linear infinite;
|
||||
}
|
1
frontend/src/components/QuizSet/index.js
Normal file
1
frontend/src/components/QuizSet/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default as QuizSet } from './QuizSet';
|
@ -1,6 +1,7 @@
|
||||
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';
|
||||
|
||||
export default function QuizsetDetail({ topic, title, quizzes = [], onDelete, onEdit }) {
|
||||
return (
|
||||
@ -21,11 +22,22 @@ export default function QuizsetDetail({ topic, title, quizzes = [], onDelete, on
|
||||
{quizzes.map((quiz, index) => (
|
||||
<div key={index}>
|
||||
<div>질문 : {quiz.question}</div>
|
||||
<img
|
||||
src={`${import.meta.env.VITE_STATIC_URL}${quiz.image}`}
|
||||
alt="강의 이미지"
|
||||
/>
|
||||
{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>
|
||||
|
@ -34,3 +34,11 @@
|
||||
font-weight: 800;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 295px;
|
||||
height: 220px;
|
||||
margin: 10px auto;
|
||||
border-radius: 8px;
|
||||
background-color: var(--background-secondary);
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
export const API_URL = import.meta.env.VITE_API_URL;
|
||||
export const ROOM_URL = import.meta.env.VITE_ROOM_URL;
|
||||
export const CHAT_URL = import.meta.env.VITE_CHAT_URL;
|
||||
export const STATIC_URL = import.meta.env.VITE_STATIC_URL;
|
||||
|
@ -31,7 +31,7 @@ export function useAuth() {
|
||||
});
|
||||
};
|
||||
|
||||
const userRegister = (role, userId, name, email, password, onError = () => {}) => {
|
||||
const userRegister = (role, userId, name, email, password) => {
|
||||
const userData = {
|
||||
role,
|
||||
userId,
|
||||
@ -39,9 +39,7 @@ export function useAuth() {
|
||||
email,
|
||||
password,
|
||||
};
|
||||
return instance.post(`${API_URL}/user/join`, userData).catch((e) => {
|
||||
onError(e);
|
||||
});
|
||||
return instance.post(`${API_URL}/user/join`, userData);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
|
@ -2,7 +2,6 @@ import instance from '../../utils/axios/instance';
|
||||
import { API_URL } from '../../constants';
|
||||
|
||||
export function usePasswordChange() {
|
||||
// TODO: API 수정 후 실제 기능하는지 확인
|
||||
const passwordChange = (currentPw, newPw, newPwCheck) => {
|
||||
const newPasswordBody = {
|
||||
currentPassword: currentPw,
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import instance from '../../utils/axios/instance';
|
||||
import { API_URL } from '../../constants';
|
||||
|
||||
export function useQuizsets() {
|
||||
return useQuery({
|
||||
return useSuspenseQuery({
|
||||
queryKey: ['quizsetList'],
|
||||
queryFn: () => instance.get(`${API_URL}/quiz`),
|
||||
});
|
||||
|
10
frontend/src/hooks/api/useStudentQuizsetDetail.js
Normal file
10
frontend/src/hooks/api/useStudentQuizsetDetail.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import instance from '../../utils/axios/instance';
|
||||
import { API_URL } from '../../constants';
|
||||
|
||||
export function useStudentQuizsetDetail(id) {
|
||||
return useSuspenseQuery({
|
||||
queryKey: ['quizset', id],
|
||||
queryFn: () => instance.get(`${API_URL}/quiz/student/${id}`),
|
||||
});
|
||||
}
|
@ -2,7 +2,7 @@ import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import instance from '../../utils/axios/instance';
|
||||
import { API_URL } from '../../constants';
|
||||
|
||||
export function useQuizsetDetail(id) {
|
||||
export function useTeacherQuizsetDetail(id) {
|
||||
return useSuspenseQuery({
|
||||
queryKey: ['quizset', id],
|
||||
queryFn: () => instance.get(`${API_URL}/quiz/teacher/${id}`),
|
10
frontend/src/hooks/api/useUserInfo.js
Normal file
10
frontend/src/hooks/api/useUserInfo.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import instance from '../../utils/axios/instance';
|
||||
import { API_URL } from '../../constants';
|
||||
|
||||
export function useUserInfo() {
|
||||
return useSuspenseQuery({
|
||||
queryKey: ['myInfo'],
|
||||
queryFn: () => instance.get(`${API_URL}/user/userinfo`),
|
||||
});
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { chatClient } from '../../utils/chat/chatClient';
|
||||
import useBoundStore from '../../store';
|
||||
import { useQuizsets } from '../api/useQuizsets';
|
||||
|
||||
const USER_ID = crypto.getRandomValues(new Uint32Array(1))[0];
|
||||
|
||||
@ -10,6 +11,19 @@ export default function useChatRoom(roomId) {
|
||||
const userName = useBoundStore((state) => state.userName) ?? '익명';
|
||||
const inputRef = useRef(null);
|
||||
const chatListRef = useRef(null);
|
||||
const { data: quizSetData } = useQuizsets();
|
||||
const quizSets = quizSetData?.data ?? [];
|
||||
const [quizSetId, setQuizSetId] = useState(null);
|
||||
|
||||
const startQuiz = (quizSetId) => {
|
||||
chatClient.publish({
|
||||
destination: `/pub/chat.quiz.${roomId}`,
|
||||
body: JSON.stringify({
|
||||
userId: USER_ID,
|
||||
quizSetId,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
@ -34,8 +48,12 @@ export default function useChatRoom(roomId) {
|
||||
client.onConnect = () => {
|
||||
client.subscribe(`/exchange/chat.exchange/*.room.${roomId}`, (response) => {
|
||||
const data = JSON.parse(response.body);
|
||||
const { content: message, name } = data;
|
||||
const { content: message, name, quizSetId } = data;
|
||||
|
||||
if (quizSetId !== undefined) {
|
||||
setQuizSetId(quizSetId);
|
||||
return;
|
||||
}
|
||||
setMessages((prev) => [...prev, { id: prev.length, text: message, isMine: USER_ID === data.userId, name }]);
|
||||
});
|
||||
};
|
||||
@ -47,7 +65,9 @@ export default function useChatRoom(roomId) {
|
||||
}, [client, roomId]);
|
||||
|
||||
useEffect(() => {
|
||||
chatListRef.current.scrollTop = chatListRef.current.scrollHeight;
|
||||
if (chatListRef.current) {
|
||||
chatListRef.current.scrollTop = chatListRef.current.scrollHeight;
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
return {
|
||||
@ -55,5 +75,9 @@ export default function useChatRoom(roomId) {
|
||||
inputRef,
|
||||
handleSubmit,
|
||||
chatListRef,
|
||||
startQuiz,
|
||||
quizSets,
|
||||
quizSetId,
|
||||
setQuizSetId,
|
||||
};
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ export default function LectureInfoPage() {
|
||||
const startDate = new Date(lectureData.startDate).toLocaleDateString();
|
||||
const endDate = new Date(lectureData.endDate).toLocaleDateString();
|
||||
const userType = useBoundStore((state) => state.userType);
|
||||
console.log(lectureData);
|
||||
const status = lectureData.status;
|
||||
const { lectureRegister } = useLectureRegister();
|
||||
const handleSubmit = () => {
|
||||
if (userType === null) {
|
||||
@ -23,15 +23,20 @@ export default function LectureInfoPage() {
|
||||
navigate('/auth/login');
|
||||
}
|
||||
|
||||
lectureRegister(lectureId)
|
||||
.then(() => {
|
||||
// navigate(`/lecture/${lectureId}`);
|
||||
window.alert('강사가 수강신청 수락시 수업이 시작됩니다.');
|
||||
navigate('/');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
if (status === 'ENROLLED') {
|
||||
navigate(`/lecture/${lectureId}`);
|
||||
}
|
||||
|
||||
if (status === 'NOT_ENROLLED') {
|
||||
lectureRegister(lectureId)
|
||||
.then(() => {
|
||||
window.alert('강사가 수강신청 수락시 수업이 시작됩니다.');
|
||||
navigate('/');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@ -58,6 +63,7 @@ export default function LectureInfoPage() {
|
||||
classTerm={`${startDate} ~ ${endDate}`}
|
||||
classTime={lectureData.time}
|
||||
onSubmit={handleSubmit}
|
||||
status={status}
|
||||
/>
|
||||
</aside>
|
||||
</MaxWidthLayout>
|
||||
|
@ -33,8 +33,14 @@ export default function LivePage() {
|
||||
connect={true}
|
||||
data-lk-theme="default"
|
||||
onDisconnected={() => {
|
||||
instance.post(`${API_URL}/video/deleteroom/${roomId}`).catch(() => {});
|
||||
window.close();
|
||||
setTimeout(() => {
|
||||
instance
|
||||
.post(`${API_URL}/video/deleteroom/${roomId}`)
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
window.close();
|
||||
});
|
||||
}, 500);
|
||||
}}
|
||||
>
|
||||
<LiveRoom />
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { InfoEditForm } from '../../components/InfoEditForm';
|
||||
import { useAuth } from '../../hooks/api/useAuth';
|
||||
import { useUserInfo } from '../../hooks/api/useUserInfo';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export default function MyInfoChangePage() {
|
||||
const navigate = useNavigate();
|
||||
const { data } = useUserInfo();
|
||||
const myInfo = data.data?.userInfo;
|
||||
const { updateInfo } = useAuth();
|
||||
|
||||
const handleSubmit = async (e, username, useremail) => {
|
||||
@ -13,5 +16,11 @@ export default function MyInfoChangePage() {
|
||||
.catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
return <InfoEditForm onSubmit={handleSubmit} />;
|
||||
return (
|
||||
<InfoEditForm
|
||||
name={myInfo.name}
|
||||
email={myInfo.email}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ export default function NoticeDetailPage() {
|
||||
const notice = data?.data;
|
||||
const navigate = useNavigate();
|
||||
const { noticeDelete } = useNoticeDelete();
|
||||
// TODO: 수정 버튼 추가(여기에 또는 ArticleDetail에)
|
||||
|
||||
const handleDelete = async () => {
|
||||
await noticeDelete(noticeId);
|
||||
|
@ -1,12 +1,30 @@
|
||||
import { PasswordChangeForm } from '../../components/PasswordChangeForm';
|
||||
import { useAuth } from '../../hooks/api/useAuth';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function PasswordChangePage() {
|
||||
const navigate = useNavigate();
|
||||
const [pwError, setPwError] = useState(false);
|
||||
const { updatePassword } = useAuth();
|
||||
const handleSubmit = async (currentPw, newPw, newPwCheck) => {
|
||||
await updatePassword(currentPw, newPw, newPwCheck).then(() => navigate('/'));
|
||||
await updatePassword(currentPw, newPw, newPwCheck)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
navigate('/');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err.response.data);
|
||||
if (err.response.data === 'Current password is incorrect') {
|
||||
console.log('현재 비밀번호 에러');
|
||||
setPwError(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
return <PasswordChangeForm onSubmit={handleSubmit} />;
|
||||
return (
|
||||
<PasswordChangeForm
|
||||
onSubmit={handleSubmit}
|
||||
pwError={pwError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,73 @@
|
||||
import { AuthForm, InputBox } from '../../components/AuthForm';
|
||||
import { useRef, useState } from 'react';
|
||||
import styles from './PasswordResetAuthPage.module.css';
|
||||
|
||||
export default function PasswordResetPage() {
|
||||
const [sentAuthNum, setSentAuthNum] = useState(false);
|
||||
const authNumRef = useRef('');
|
||||
const passwordRef = useRef('');
|
||||
const passwordConfirmRef = useRef('');
|
||||
|
||||
const [passwordMatch, setPasswordMatch] = useState(true);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
console.log(authNumRef.current.value);
|
||||
authNumRef.current.value = '';
|
||||
setSentAuthNum(true);
|
||||
};
|
||||
|
||||
const handlePost = async (e) => {
|
||||
e.preventDefault();
|
||||
const isPWMatch = passwordRef.current.value === passwordConfirmRef.current.value;
|
||||
|
||||
setPasswordMatch(isPWMatch);
|
||||
if (!isPWMatch) {
|
||||
return;
|
||||
}
|
||||
console.log(passwordRef.current.value, passwordConfirmRef.current.value);
|
||||
};
|
||||
|
||||
return sentAuthNum ? (
|
||||
<div className={styles.wrapper}>
|
||||
<AuthForm
|
||||
onSubmit={handlePost}
|
||||
title="비밀번호 변경"
|
||||
buttonText="비밀번호 변경"
|
||||
>
|
||||
<InputBox
|
||||
title="새 비밀번호"
|
||||
type="password"
|
||||
id="password"
|
||||
ref={passwordRef}
|
||||
/>
|
||||
<InputBox
|
||||
title="새 비밀번호 확인"
|
||||
type="password"
|
||||
id="passwordConfirm"
|
||||
ref={passwordConfirmRef}
|
||||
hasError={!passwordMatch}
|
||||
>
|
||||
{!passwordMatch && (
|
||||
<div className={`${styles.textBodyStrong} ${styles.dangerColor}`}>비밀번호가 일치하지 않습니다</div>
|
||||
)}
|
||||
</InputBox>
|
||||
</AuthForm>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.wrapper}>
|
||||
<AuthForm
|
||||
onSubmit={handleSubmit}
|
||||
title="인증번호 입력"
|
||||
buttonText="인증번호 입력"
|
||||
>
|
||||
<InputBox
|
||||
title="인증번호"
|
||||
id="authNum"
|
||||
type="password"
|
||||
ref={authNumRef}
|
||||
/>
|
||||
</AuthForm>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
.wrapper,
|
||||
.loginGroup {
|
||||
padding-top: 20px;
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.loginGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 20px 40px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36px;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.text {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.linkButton {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background-color: var(--primary-color);
|
||||
border: none;
|
||||
color: var(--on-primary);
|
||||
padding: 12px;
|
||||
margin-top: 20px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
1
frontend/src/pages/PasswordResetAuthPage/index.js
Normal file
1
frontend/src/pages/PasswordResetAuthPage/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './PasswordResetAuthPage';
|
@ -1,35 +1,52 @@
|
||||
import { AuthForm, InputBox } from '../../components/AuthForm';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import styles from './PasswordResetPage.module.css';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
export default function PasswordResetPage() {
|
||||
const navigate = useNavigate();
|
||||
const [time, setTime] = useState(5);
|
||||
const [sendEmail, setSendEmail] = useState(false);
|
||||
const emailRef = useRef('');
|
||||
const buttonText = useRef('비밀번호 찾기');
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
// TODO: 비밀번호 찾기 POST 기능 추가
|
||||
console.log('비밀번호 찾기', emailRef.current.value);
|
||||
// delay
|
||||
setTimeout(() => {
|
||||
setSendEmail(true);
|
||||
}, 200);
|
||||
setSendEmail(true);
|
||||
};
|
||||
|
||||
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 ? (
|
||||
<section className={styles.loginGroup}>
|
||||
<h1 className={styles.title}>비밀번호 찾기</h1>
|
||||
<p className={styles.text}>
|
||||
비밀번호 초기화 이메일을 보냈습니다.
|
||||
비밀번호 초기화 인증번호를 이메일로 보냈습니다.
|
||||
<br />
|
||||
메일함을 확인해주세요.
|
||||
<br />
|
||||
<span className={styles.seconds}>{time}초</span> 후에 자동으로 인증번호 입력 페이지로 이동합니다.
|
||||
</p>
|
||||
<Link
|
||||
to={'../login'}
|
||||
to={'../resetAuth'}
|
||||
className={styles.linkButton}
|
||||
>
|
||||
로그인하러 가기
|
||||
인증번호 입력하러 가기
|
||||
</Link>
|
||||
</section>
|
||||
) : (
|
||||
|
@ -46,3 +46,10 @@
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.seconds {
|
||||
color: var(--primary-color);
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useQuizsetDetail } from '../../hooks/api/useQuizsetDetail';
|
||||
import { useTeacherQuizsetDetail } from '../../hooks/api/useTeacherQuizsetDetail';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { QuizsetDetail } from '../../components/QuizsetDetail';
|
||||
import { useQuizsetDelete } from '../../hooks/api/useQuizsetDelete';
|
||||
@ -7,7 +7,7 @@ export default function QuizsetDetailPage() {
|
||||
const navigate = useNavigate();
|
||||
const { quizsetId } = useParams();
|
||||
const { quizsetDelete } = useQuizsetDelete();
|
||||
const { data } = useQuizsetDetail(quizsetId);
|
||||
const { data } = useTeacherQuizsetDetail(quizsetId);
|
||||
const quizset = data.data;
|
||||
console.log(quizset);
|
||||
const handleEdit = () => {
|
||||
|
@ -11,7 +11,6 @@ export default function StudentHomePage() {
|
||||
const { data: allLectures } = useLectures();
|
||||
const allClasses = allLectures?.data ?? [];
|
||||
|
||||
// TODO: 전체 강의 안에 수강중인 강의가 나옴
|
||||
return (
|
||||
<MaxWidthLayout>
|
||||
<ClassGrid title="수강중인 강의">
|
||||
|
@ -15,14 +15,16 @@ export default function UserRegisterPage() {
|
||||
|
||||
const [userType, setUserType] = useState('STUDENT');
|
||||
const [passwordMatch, setPasswordMatch] = useState(true);
|
||||
const [existingId, setExistingId] = useState(false);
|
||||
const [existingEmail, setExistingEmail] = useState(false);
|
||||
|
||||
const { userRegister } = useAuth();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
// TODO: 회원가입 POST 기능 추가
|
||||
const isPWMatch = passwordRef.current.value === passwordConfirmRef.current.value;
|
||||
|
||||
setExistingId(false);
|
||||
setExistingEmail(false);
|
||||
setPasswordMatch(isPWMatch);
|
||||
if (!isPWMatch) {
|
||||
return;
|
||||
@ -33,10 +35,18 @@ export default function UserRegisterPage() {
|
||||
nameRef.current.value,
|
||||
emailRef.current.value,
|
||||
passwordRef.current.value
|
||||
).then((response) => {
|
||||
console.log(response);
|
||||
navigate('../login');
|
||||
});
|
||||
)
|
||||
.then(() => {
|
||||
navigate('../login');
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response.data === '아이디가 중복 됐습니다.') {
|
||||
setExistingId(true);
|
||||
}
|
||||
if (err.response.data === '이메일이 중복 됐습니다.') {
|
||||
setExistingEmail(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const linkProps = {
|
||||
@ -74,7 +84,12 @@ export default function UserRegisterPage() {
|
||||
type="text"
|
||||
id="ID"
|
||||
ref={idRef}
|
||||
/>
|
||||
hasError={existingId}
|
||||
>
|
||||
{existingId && (
|
||||
<div className={`${styles.textBodyStrong} ${styles.dangerColor}`}>이미 존재하는 아이디입니다</div>
|
||||
)}
|
||||
</InputBox>
|
||||
<InputBox
|
||||
title="이름"
|
||||
type="text"
|
||||
@ -86,7 +101,12 @@ export default function UserRegisterPage() {
|
||||
type="email"
|
||||
id="email"
|
||||
ref={emailRef}
|
||||
/>
|
||||
hasError={existingEmail}
|
||||
>
|
||||
{existingEmail && (
|
||||
<div className={`${styles.textBodyStrong} ${styles.dangerColor}`}>이미 등록된 이메일입니다</div>
|
||||
)}
|
||||
</InputBox>
|
||||
<InputBox
|
||||
title="비밀번호"
|
||||
type="password"
|
||||
|
Loading…
Reference in New Issue
Block a user