Merge branch 'frontend' into 'fe/studentReport'
# Conflicts: # frontend/src/Router.jsx
This commit is contained in:
commit
04df5b2ccd
@ -3,9 +3,9 @@ import { createBrowserRouter } from 'react-router-dom';
|
|||||||
import PageLayout from './components/Layout/PageLayout';
|
import PageLayout from './components/Layout/PageLayout';
|
||||||
import HomePage from './pages/HomePage';
|
import HomePage from './pages/HomePage';
|
||||||
import NotFoundPage from './pages/NotFoundPage';
|
import NotFoundPage from './pages/NotFoundPage';
|
||||||
import { lazy } from 'react';
|
import { lazy, Suspense } from 'react';
|
||||||
import MyPageLayout from './components/Layout/MyPageLayout';
|
import MyPageLayout from './components/Layout/MyPageLayout';
|
||||||
import LivePage from './pages/LivePage';
|
// import LivePage from './pages/LivePage';
|
||||||
import ErrorPage from './pages/ErrorPage';
|
import ErrorPage from './pages/ErrorPage';
|
||||||
import { LectureLayout } from './components/Layout';
|
import { LectureLayout } from './components/Layout';
|
||||||
|
|
||||||
@ -38,6 +38,7 @@ const FreeboardDetailPage = lazy(async () => await import('./pages/FreeboardDeta
|
|||||||
const EditFreeboardPage = lazy(async () => await import('./pages/EditFreeboardPage'));
|
const EditFreeboardPage = lazy(async () => await import('./pages/EditFreeboardPage'));
|
||||||
const PasswordResetAuthPage = lazy(async () => await import('./pages/PasswordResetAuthPage'));
|
const PasswordResetAuthPage = lazy(async () => await import('./pages/PasswordResetAuthPage'));
|
||||||
const StudentReportPage = lazy(async () => await import('./pages/StudentReportPage'));
|
const StudentReportPage = lazy(async () => await import('./pages/StudentReportPage'));
|
||||||
|
const LivePage = lazy(async () => await import('./pages/LivePage'));
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -46,7 +47,11 @@ const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'live/:roomId',
|
path: 'live/:roomId',
|
||||||
element: <LivePage />,
|
element: (
|
||||||
|
<Suspense fallback={<></>}>
|
||||||
|
<LivePage />
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
|
@ -3,7 +3,6 @@ import { Link } from 'react-router-dom';
|
|||||||
import styles from './ArticleDetail.module.css';
|
import styles from './ArticleDetail.module.css';
|
||||||
import ArticleDetailAnswer from './ArticleDetailAnswer/ArticleDetailAnswer';
|
import ArticleDetailAnswer from './ArticleDetailAnswer/ArticleDetailAnswer';
|
||||||
import ArticleDetailAnswerInput from './ArticleDetailAnswer/ArticleDetailAnswerInput';
|
import ArticleDetailAnswerInput from './ArticleDetailAnswer/ArticleDetailAnswerInput';
|
||||||
import EditIcon from '/src/assets/icons/edit.svg?react';
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
export default function ArticleDetail({ topic, title, author = null, content, answer = null, onDelete, isQna = true }) {
|
export default function ArticleDetail({ topic, title, author = null, content, answer = null, onDelete, isQna = true }) {
|
||||||
@ -44,22 +43,22 @@ export default function ArticleDetail({ topic, title, author = null, content, an
|
|||||||
{author && <span className={styles.author}>{author}</span>}
|
{author && <span className={styles.author}>{author}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<div className={styles.actionGroup}>
|
||||||
type="button"
|
<Link
|
||||||
className={styles.editButton}
|
className={styles.edit}
|
||||||
to={'edit'}
|
to={'edit'}
|
||||||
state={{ title: title, content: content, answer: answer }}
|
state={{ title: title, content: content, answer: answer }}
|
||||||
>
|
>
|
||||||
<EditIcon className={styles.icon} />
|
수정
|
||||||
<span>수정하기</span>
|
</Link>
|
||||||
</Link>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className={styles.delete}
|
||||||
className={styles.deleteButton}
|
onClick={onDelete}
|
||||||
onClick={onDelete}
|
>
|
||||||
>
|
삭제
|
||||||
삭제하기
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div>
|
<div>
|
||||||
<p className={styles.content}>{content}</p>
|
<p className={styles.content}>{content}</p>
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: start;
|
align-items: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerInside {
|
.headerInside {
|
||||||
@ -60,34 +60,26 @@
|
|||||||
stroke: var(--text-color);
|
stroke: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.editButton {
|
.actionGroup {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: end;
|
||||||
gap: 8px;
|
gap: 20px;
|
||||||
padding: 12px;
|
|
||||||
background: var(--background);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.4;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit,
|
||||||
|
.delete {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
background-color: var(--background);
|
||||||
|
color: var(--text-color-tertiary);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.deleteButton {
|
.delete {
|
||||||
display: flex;
|
color: var(--error-color);
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border: 1px solid var(--error-color);
|
|
||||||
background-color: var(--error-color);
|
|
||||||
color: var(--on-primary);
|
|
||||||
stroke: var(--error-color);
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1.4;
|
|
||||||
font-weight: 700;
|
|
||||||
align-self: end;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
@ -15,28 +15,29 @@ export default function ArticleDetailAnswer({ answer, onEditClick, onDeleteSubmi
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<section className={styles.answer}>
|
||||||
<section className={styles.answer}>
|
<div className={styles.answerHeader}>
|
||||||
<div className={styles.answerHeader}>
|
<div className={styles.author}>
|
||||||
<ReplyIcon />
|
<ReplyIcon /> <span>선생님의 답변</span>
|
||||||
<div className={styles.author}>선생님의 답변</div>
|
|
||||||
</div>
|
</div>
|
||||||
<p className={styles.content}>{answer}</p>
|
<div className={styles.actionGroup}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.deleteButton}
|
className={styles.edit}
|
||||||
onClick={handleDeleteSubmit}
|
onClick={onEditClick}
|
||||||
>
|
>
|
||||||
<div>삭제</div>
|
수정
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.editButton}
|
className={styles.delete}
|
||||||
onClick={onEditClick}
|
onClick={handleDeleteSubmit}
|
||||||
>
|
>
|
||||||
수정
|
<div>삭제</div>
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</div>
|
||||||
</>
|
</div>
|
||||||
|
<p className={styles.content}>{answer}</p>
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
.answer {
|
.answer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 12px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
@ -11,17 +11,26 @@
|
|||||||
|
|
||||||
.answerHeader {
|
.answerHeader {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
justify-content: space-between;
|
||||||
color: var(--text-color-secondary);
|
color: var(--text-color-secondary);
|
||||||
stroke: var(--text-color-secondary);
|
stroke: var(--text-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.author {
|
.author {
|
||||||
|
align-self: start;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actionGroup {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@ -30,36 +39,19 @@
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.editButton {
|
.edit,
|
||||||
display: flex;
|
.delete {
|
||||||
align-items: center;
|
padding: 0;
|
||||||
gap: 8px;
|
margin: 0;
|
||||||
padding: 12px 16px;
|
border: none;
|
||||||
border: 1px solid var(--primary-color);
|
background-color: var(--background);
|
||||||
background-color: var(--primary-color);
|
font-size: 14px;
|
||||||
color: var(--on-primary);
|
|
||||||
stroke: var(--on-primary);
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
font-weight: 700;
|
font-weight: 500;
|
||||||
align-self: end;
|
color: var(--text-color-tertiary);
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.deleteButton {
|
.delete {
|
||||||
display: flex;
|
color: var(--error-color);
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border: 1px solid var(--error-color);
|
|
||||||
background-color: var(--error-color);
|
|
||||||
color: var(--on-primary);
|
|
||||||
stroke: var(--on-primary);
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1.4;
|
|
||||||
font-weight: 700;
|
|
||||||
align-self: end;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ import styles from './ArticlePreview.module.css';
|
|||||||
import RightIcon from '/src/assets/icons/right.svg?react';
|
import RightIcon from '/src/assets/icons/right.svg?react';
|
||||||
|
|
||||||
export default function ArticlePreview({ to, title, contents = [] }) {
|
export default function ArticlePreview({ to, title, contents = [] }) {
|
||||||
|
const hasContents = contents.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<Link
|
<Link
|
||||||
@ -13,17 +15,21 @@ export default function ArticlePreview({ to, title, contents = [] }) {
|
|||||||
<RightIcon />
|
<RightIcon />
|
||||||
</Link>
|
</Link>
|
||||||
<div className={styles.main}>
|
<div className={styles.main}>
|
||||||
{contents.map?.((content) => {
|
{hasContents ? (
|
||||||
return (
|
contents.map?.((content) => {
|
||||||
<Link
|
return (
|
||||||
to={`${to}/${content.id}`}
|
<Link
|
||||||
key={content.id}
|
to={`${to}/${content.id}`}
|
||||||
className={styles.content}
|
key={content.id}
|
||||||
>
|
className={styles.content}
|
||||||
{content.title}
|
>
|
||||||
</Link>
|
{content.title}
|
||||||
);
|
</Link>
|
||||||
})}
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className={styles.empty}>아직 작성된 글이 없습니다.</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -35,3 +35,13 @@
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
color: var(--text-color-tertiary);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
@ -18,7 +18,9 @@ export default function ArticleBoard({ title, canCreate, children }) {
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.article}>{children}</div>
|
<div className={styles.article}>
|
||||||
|
{children ? children : <div className={styles.empty}>표시할 내용이 없습니다.</div>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -47,3 +47,13 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 60px 0;
|
||||||
|
color: var(--text-color-tertiary);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
@ -69,7 +69,7 @@ export default function ChatRoom({ isTeacher, ...props }) {
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
className={`lk-chat ${wsChat.quizSetId ? styles.none : ''}`}
|
className={`lk-chat ${wsChat.quizSetInfo ? styles.none : ''}`}
|
||||||
>
|
>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<h2 className={styles.title}>채팅</h2>
|
<h2 className={styles.title}>채팅</h2>
|
||||||
@ -131,15 +131,16 @@ export default function ChatRoom({ isTeacher, ...props }) {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{wsChat.quizSetId && (
|
{wsChat.quizSetInfo && (
|
||||||
<div className="lk-chat">
|
<div className="lk-chat">
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<h2 className={styles.title}>퀴즈</h2>
|
<h2 className={styles.title}>퀴즈</h2>
|
||||||
</header>
|
</header>
|
||||||
<Suspense fallback={<></>}>
|
<Suspense fallback={<></>}>
|
||||||
<QuizSet
|
<QuizSet
|
||||||
quizSetId={wsChat.quizSetId}
|
quizSetId={wsChat.quizSetInfo[0]}
|
||||||
finish={() => wsChat.setQuizSetId(null)}
|
reportSetId={wsChat.quizSetInfo[1]}
|
||||||
|
finish={() => wsChat.setQuizSetInfo(null)}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,3 +19,7 @@
|
|||||||
stroke: var(--text-color);
|
stroke: var(--text-color);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img.thumbnail {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
22
frontend/src/components/Countdown/Countdown.jsx
Normal file
22
frontend/src/components/Countdown/Countdown.jsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export default function Countdown({ seconds }) {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
const [remainedTime, setRemainedTime] = useState(seconds);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setRemainedTime((prev) => {
|
||||||
|
if (prev === 0) {
|
||||||
|
clearInterval(interval);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [remainedTime]);
|
||||||
|
|
||||||
|
return <span>{remainedTime}</span>;
|
||||||
|
}
|
@ -26,12 +26,6 @@ export default function Header() {
|
|||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<Link to={'/'}>전체 강의</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link to={'/'}>수강중인 강의</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
to={'/live/1'}
|
to={'/live/1'}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import styles from './LectureLayout.module.css';
|
||||||
import { Outlet, useParams } from 'react-router-dom';
|
import { Outlet, useParams } from 'react-router-dom';
|
||||||
import LectureHeader from '../LectureHeader/LectureHeader';
|
import LectureHeader from '../LectureHeader/LectureHeader';
|
||||||
import { SideBar, SideLink, SideItem } from '../SideBar';
|
import { SideBar, SideLink, SideItem } from '../SideBar';
|
||||||
@ -18,9 +19,11 @@ export default function LectureLayout() {
|
|||||||
const lecture = data?.data;
|
const lecture = data?.data;
|
||||||
console.log(lecture);
|
console.log(lecture);
|
||||||
const userType = useBoundStore((state) => state.userType);
|
const userType = useBoundStore((state) => state.userType);
|
||||||
const handleDelete = async () => {
|
const handleDelete = () => {
|
||||||
await lectureDelete(lectureId);
|
confirm('강의를 삭제할까요??') &&
|
||||||
navigate('..');
|
lectureDelete(lectureId).then(() => {
|
||||||
|
navigate('..');
|
||||||
|
});
|
||||||
};
|
};
|
||||||
const lectureData = {
|
const lectureData = {
|
||||||
title: lecture.title,
|
title: lecture.title,
|
||||||
@ -61,13 +64,24 @@ export default function LectureLayout() {
|
|||||||
name="수강생"
|
name="수강생"
|
||||||
sub="총 12명"
|
sub="총 12명"
|
||||||
/>
|
/>
|
||||||
|
</SideBar>
|
||||||
|
)}
|
||||||
|
{userType === 'teacher' && (
|
||||||
|
<SideBar title={'강의 정보 관리'}>
|
||||||
<SideLink
|
<SideLink
|
||||||
to={'edit'}
|
to={'edit'}
|
||||||
state={lectureData}
|
state={lectureData}
|
||||||
>
|
>
|
||||||
강의 정보 수정
|
강의 정보 수정
|
||||||
</SideLink>
|
</SideLink>
|
||||||
<button onClick={handleDelete}>강의 삭제</button>
|
<li>
|
||||||
|
<span
|
||||||
|
onClick={handleDelete}
|
||||||
|
className={styles.delete}
|
||||||
|
>
|
||||||
|
강의 삭제
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
</SideBar>
|
</SideBar>
|
||||||
)}
|
)}
|
||||||
{userType === 'student' && (
|
{userType === 'student' && (
|
||||||
|
7
frontend/src/components/Layout/LectureLayout.module.css
Normal file
7
frontend/src/components/Layout/LectureLayout.module.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.delete {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--error-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
min-width: 1360px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,21 +8,39 @@ export default function LectureEnroll({ userName, enrollid, onDelete }) {
|
|||||||
|
|
||||||
const handleAccept = async (e) => {
|
const handleAccept = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!confirm('수강신청을 승인하시겠습니까?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await lectureEnrollAccept(enrollid);
|
await lectureEnrollAccept(enrollid);
|
||||||
onDelete(enrollid);
|
onDelete(enrollid);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = async (e) => {
|
const handleCancel = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!confirm('수강신청을 거절하시겠습니까?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await lectureEnrollCancel(enrollid);
|
await lectureEnrollCancel(enrollid);
|
||||||
onDelete(enrollid);
|
onDelete(enrollid);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.enrollLink}>
|
<div className={styles.enrollLink}>
|
||||||
<p>{userName}</p>
|
<span>{userName}</span>
|
||||||
<button onClick={handleAccept}>등록</button>
|
<div className={styles.buttonWrapper}>
|
||||||
<button onClick={handleCancel}>삭제</button>
|
<button
|
||||||
|
onClick={handleAccept}
|
||||||
|
className={styles.accept}
|
||||||
|
>
|
||||||
|
등록
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleCancel}
|
||||||
|
className={styles.reject}
|
||||||
|
>
|
||||||
|
삭제
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,44 @@
|
|||||||
.enrollLink {
|
.enrollLink {
|
||||||
border-radius: 8px;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
box-sizing: border-box;
|
align-items: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 100%;
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
transition: background-color 0.25s;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.enrollLink:hover {
|
.buttonWrapper {
|
||||||
background-color: var(--background-secondary);
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accept,
|
||||||
|
.reject {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background-color: var(--background);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color 0.1s,
|
||||||
|
color 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accept:hover {
|
||||||
|
border-color: var(--info-color);
|
||||||
|
color: var(--info-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reject:hover {
|
||||||
|
border-color: var(--error-color);
|
||||||
|
color: var(--error-color);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { useState } from 'react';
|
|||||||
import { STATIC_URL } from '../../constants';
|
import { STATIC_URL } from '../../constants';
|
||||||
import styles from './Quiz.module.css';
|
import styles from './Quiz.module.css';
|
||||||
|
|
||||||
export default function Quiz({ question, step, image, choices = [], setAnswers }) {
|
export default function Quiz({ question, image, choices = [], setAnswers }) {
|
||||||
const [answer, setAnswer] = useState(null);
|
const [answer, setAnswer] = useState(null);
|
||||||
const isChoice = choices.length > 0;
|
const isChoice = choices.length > 0;
|
||||||
|
|
||||||
@ -37,9 +37,9 @@ export default function Quiz({ question, step, image, choices = [], setAnswers }
|
|||||||
type="text"
|
type="text"
|
||||||
autoFocus
|
autoFocus
|
||||||
placeholder="답 입력"
|
placeholder="답 입력"
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
setAnswers((prev) => prev.map((value, index) => (index === step ? e.target.value : value)))
|
setAnswers(e.target.value);
|
||||||
}
|
}}
|
||||||
className={styles.input}
|
className={styles.input}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import styles from './QuizSet.module.css';
|
import styles from './QuizSet.module.css';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import Quiz from '../Quiz/Quiz';
|
import Quiz from '../Quiz/Quiz';
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import LoadingIndicator from '../LoadingIndicator.jsx/LoadingIndicator';
|
import LoadingIndicator from '../LoadingIndicator.jsx/LoadingIndicator';
|
||||||
import instance from '../../utils/axios/instance';
|
import instance from '../../utils/axios/instance';
|
||||||
import { API_URL } from '../../constants';
|
import { API_URL } from '../../constants';
|
||||||
import { useStudentQuizsetDetail } from '../../hooks/api/useStudentQuizsetDetail';
|
import { useStudentQuizsetDetail } from '../../hooks/api/useStudentQuizsetDetail';
|
||||||
|
import Countdown from '../Countdown/Countdown';
|
||||||
|
|
||||||
export default function QuizSet({ quizSetId, finish }) {
|
export default function QuizSet({ quizSetId, reportSetId, finish }) {
|
||||||
const { roomId } = useParams();
|
|
||||||
const [step, setStep] = useState(null);
|
const [step, setStep] = useState(null);
|
||||||
const { data } = useStudentQuizsetDetail(quizSetId);
|
const { data } = useStudentQuizsetDetail(quizSetId);
|
||||||
const quizSetData = data?.data;
|
const quizSetData = data?.data;
|
||||||
@ -17,15 +16,17 @@ export default function QuizSet({ quizSetId, finish }) {
|
|||||||
const interval = useRef(null);
|
const interval = useRef(null);
|
||||||
const submit = useCallback(
|
const submit = useCallback(
|
||||||
(data) => {
|
(data) => {
|
||||||
instance.post(`${API_URL}/report/submit/${roomId}/quizset/${quizSetId}`, data).catch(() => {});
|
const requestData = {
|
||||||
|
answer: data,
|
||||||
|
};
|
||||||
|
instance.post(`${API_URL}/report/submit/${reportSetId}/quizset/${quizSetId}`, requestData).catch(() => {});
|
||||||
},
|
},
|
||||||
[quizSetId, roomId]
|
[quizSetId, reportSetId]
|
||||||
);
|
);
|
||||||
const QuizComponents = [
|
const QuizComponents = [
|
||||||
...quizList.map((quiz, index) => (
|
...quizList.map((quiz, index) => (
|
||||||
<Quiz
|
<Quiz
|
||||||
key={index}
|
key={index}
|
||||||
step={index}
|
|
||||||
answers={answers.current}
|
answers={answers.current}
|
||||||
setAnswers={(value) => {
|
setAnswers={(value) => {
|
||||||
answers.current = answers.current.map((v, i) => (i === index ? value : v));
|
answers.current = answers.current.map((v, i) => (i === index ? value : v));
|
||||||
@ -37,7 +38,10 @@ export default function QuizSet({ quizSetId, finish }) {
|
|||||||
key={Infinity}
|
key={Infinity}
|
||||||
className={styles.message}
|
className={styles.message}
|
||||||
>
|
>
|
||||||
퀴즈 종료
|
<div>
|
||||||
|
<div>퀴즈 종료!</div>
|
||||||
|
<div className={styles.subMsg}>답안을 전송하고 있어요</div>
|
||||||
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -60,7 +64,7 @@ export default function QuizSet({ quizSetId, finish }) {
|
|||||||
|
|
||||||
return prev + 1;
|
return prev + 1;
|
||||||
});
|
});
|
||||||
}, 5000);
|
}, 10 * 1000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(interval.current);
|
clearInterval(interval.current);
|
||||||
@ -78,7 +82,9 @@ export default function QuizSet({ quizSetId, finish }) {
|
|||||||
<>
|
<>
|
||||||
{step === null ? (
|
{step === null ? (
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>
|
||||||
<span>퀴즈를 시작합니다</span>
|
<span>
|
||||||
|
<Countdown seconds={10} />초 후 퀴즈를 시작합니다.
|
||||||
|
</span>
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
gap: 32px;
|
gap: 32px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@ -40,5 +41,24 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
transform-origin: left center;
|
transform-origin: left center;
|
||||||
animation: progress 5s linear infinite;
|
animation: progress 10s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shake {
|
||||||
|
0% {
|
||||||
|
rotate: -2.5deg;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
rotate: 2.5deg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subMsg {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #ccc;
|
||||||
|
animation: shake 1s ease-in-out alternate-reverse infinite;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ export default function SideItem({ name, sub }) {
|
|||||||
return (
|
return (
|
||||||
<li className={styles.item}>
|
<li className={styles.item}>
|
||||||
<div>{name}</div>
|
<div>{name}</div>
|
||||||
<div className={styles.sub}>{sub}</div>
|
{sub && <div className={styles.sub}>{sub}</div>}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,29 +6,24 @@ export function useAuth() {
|
|||||||
const setToken = useBoundStore((state) => state.setToken);
|
const setToken = useBoundStore((state) => state.setToken);
|
||||||
const setUserType = useBoundStore((state) => state.setUserType);
|
const setUserType = useBoundStore((state) => state.setUserType);
|
||||||
|
|
||||||
const login = (userId, password, onError = () => {}) => {
|
const login = (userId, password) => {
|
||||||
const formData = {
|
const formData = {
|
||||||
userId,
|
userId,
|
||||||
password,
|
password,
|
||||||
};
|
};
|
||||||
|
|
||||||
return instance
|
return instance.post(`${API_URL}/user/login`, formData).then(({ data, config }) => {
|
||||||
.post(`${API_URL}/user/login`, formData)
|
const { role: role, 'access-token': accessToken } = data;
|
||||||
.then(({ data, config }) => {
|
config.headers.Authorization = `${accessToken}`;
|
||||||
const { role: role, 'access-token': accessToken } = data;
|
setToken(accessToken);
|
||||||
config.headers.Authorization = `${accessToken}`;
|
|
||||||
setToken(accessToken);
|
|
||||||
|
|
||||||
if (role === 'ADMIN') {
|
if (role === 'ADMIN') {
|
||||||
setUserType('teacher');
|
setUserType('teacher');
|
||||||
} else if (role === 'STUDENT') {
|
} else if (role === 'STUDENT') {
|
||||||
setUserType('student');
|
setUserType('student');
|
||||||
}
|
}
|
||||||
})
|
return accessToken;
|
||||||
.catch((e) => {
|
});
|
||||||
alert('아이디 또는 비밀번호를 다시 확인해주세요.');
|
|
||||||
onError(e);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const userRegister = (role, userId, name, email, password) => {
|
const userRegister = (role, userId, name, email, password) => {
|
||||||
|
@ -4,7 +4,7 @@ import { API_URL } from '../../constants';
|
|||||||
|
|
||||||
export function useLectureEnroll(lectureId) {
|
export function useLectureEnroll(lectureId) {
|
||||||
return useSuspenseQuery({
|
return useSuspenseQuery({
|
||||||
queryKey: ['lecturelist', lectureId],
|
queryKey: ['enroll', lectureId],
|
||||||
queryFn: () => instance.get(`${API_URL}/registration/${lectureId}`),
|
queryFn: () => instance.get(`${API_URL}/registration/${lectureId}`),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import { API_URL } from '../../constants';
|
|||||||
|
|
||||||
export function useLectureInfo(lectureId) {
|
export function useLectureInfo(lectureId) {
|
||||||
return useSuspenseQuery({
|
return useSuspenseQuery({
|
||||||
queryKey: ['lecturelist', lectureId],
|
queryKey: ['lectureInfo', lectureId],
|
||||||
queryFn: () => instance.get(`${API_URL}/lecture/${lectureId}`),
|
queryFn: () => instance.get(`${API_URL}/lecture/${lectureId}`),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,6 @@ import { chatClient } from '../../utils/chat/chatClient';
|
|||||||
import useBoundStore from '../../store';
|
import useBoundStore from '../../store';
|
||||||
import { useQuizsets } from '../api/useQuizsets';
|
import { useQuizsets } from '../api/useQuizsets';
|
||||||
|
|
||||||
const USER_ID = crypto.getRandomValues(new Uint32Array(1))[0];
|
|
||||||
|
|
||||||
export default function useChatRoom(roomId) {
|
export default function useChatRoom(roomId) {
|
||||||
const client = chatClient;
|
const client = chatClient;
|
||||||
const [messages, setMessages] = useState([]);
|
const [messages, setMessages] = useState([]);
|
||||||
@ -13,13 +11,12 @@ export default function useChatRoom(roomId) {
|
|||||||
const chatListRef = useRef(null);
|
const chatListRef = useRef(null);
|
||||||
const { data: quizSetData } = useQuizsets();
|
const { data: quizSetData } = useQuizsets();
|
||||||
const quizSets = quizSetData?.data ?? [];
|
const quizSets = quizSetData?.data ?? [];
|
||||||
const [quizSetId, setQuizSetId] = useState(null);
|
const [quizSetInfo, setQuizSetInfo] = useState(null);
|
||||||
|
|
||||||
const startQuiz = (quizSetId) => {
|
const startQuiz = (quizSetId) => {
|
||||||
chatClient.publish({
|
chatClient.publish({
|
||||||
destination: `/pub/chat.quiz.${roomId}`,
|
destination: `/pub/chat.quiz.${roomId}`,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
userId: USER_ID,
|
|
||||||
quizSetId,
|
quizSetId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@ -36,7 +33,6 @@ export default function useChatRoom(roomId) {
|
|||||||
chatClient.publish({
|
chatClient.publish({
|
||||||
destination: `/pub/chat.message.${roomId}`,
|
destination: `/pub/chat.message.${roomId}`,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
userId: USER_ID,
|
|
||||||
name: userName,
|
name: userName,
|
||||||
content: text,
|
content: text,
|
||||||
}),
|
}),
|
||||||
@ -48,13 +44,13 @@ export default function useChatRoom(roomId) {
|
|||||||
client.onConnect = () => {
|
client.onConnect = () => {
|
||||||
client.subscribe(`/exchange/chat.exchange/*.room.${roomId}`, (response) => {
|
client.subscribe(`/exchange/chat.exchange/*.room.${roomId}`, (response) => {
|
||||||
const data = JSON.parse(response.body);
|
const data = JSON.parse(response.body);
|
||||||
const { content: message, name, quizSetId } = data;
|
const { content: message, name, quizSetId, reportSetId } = data;
|
||||||
|
|
||||||
if (quizSetId !== undefined) {
|
if (quizSetId !== undefined) {
|
||||||
setQuizSetId(quizSetId);
|
setQuizSetInfo([quizSetId, reportSetId]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setMessages((prev) => [...prev, { id: prev.length, text: message, isMine: USER_ID === data.userId, name }]);
|
setMessages((prev) => [...prev, { id: prev.length, text: message, name }]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
client.activate();
|
client.activate();
|
||||||
@ -77,7 +73,7 @@ export default function useChatRoom(roomId) {
|
|||||||
chatListRef,
|
chatListRef,
|
||||||
startQuiz,
|
startQuiz,
|
||||||
quizSets,
|
quizSets,
|
||||||
quizSetId,
|
quizSetInfo,
|
||||||
setQuizSetId,
|
setQuizSetInfo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,9 @@ import { useQnas } from '../../hooks/api/useQnas';
|
|||||||
export default function LearningLectureDetailPage() {
|
export default function LearningLectureDetailPage() {
|
||||||
const { lectureId } = useParams();
|
const { lectureId } = useParams();
|
||||||
const { data: noticesData } = useNotices(lectureId);
|
const { data: noticesData } = useNotices(lectureId);
|
||||||
const notices = noticesData?.data;
|
const notices = noticesData?.data.slice(0, 3);
|
||||||
const { data: qnasData } = useQnas(lectureId);
|
const { data: qnasData } = useQnas(lectureId);
|
||||||
const questions = qnasData?.data;
|
const questions = qnasData?.data.slice(0, 3);
|
||||||
// TODO: QnA 훅 작성 후 사용 및 3개까지만 slice 추가
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.previews}>
|
<section className={styles.previews}>
|
||||||
@ -24,7 +23,6 @@ export default function LearningLectureDetailPage() {
|
|||||||
title="Q&A"
|
title="Q&A"
|
||||||
contents={questions}
|
contents={questions}
|
||||||
/>
|
/>
|
||||||
<ArticlePreview title="커리큘럼" />
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,21 +5,26 @@ import { useMyLectures } from '../../hooks/api/useMyLectures';
|
|||||||
export default function LearningLecturesPage() {
|
export default function LearningLecturesPage() {
|
||||||
const { data } = useMyLectures();
|
const { data } = useMyLectures();
|
||||||
const onGoingClasses = data?.data ?? [];
|
const onGoingClasses = data?.data ?? [];
|
||||||
|
const hasOnGoingClasses = onGoingClasses.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h2 className={styles.title}>수강중인 강의</h2>
|
<h2 className={styles.title}>수강중인 강의</h2>
|
||||||
<div className={styles.grid}>
|
<div className={styles.grid}>
|
||||||
{onGoingClasses.map?.((lecture) => (
|
{hasOnGoingClasses ? (
|
||||||
<Link
|
onGoingClasses.map?.((lecture) => (
|
||||||
key={lecture.id}
|
<Link
|
||||||
to={`/lecture/${lecture.id}`}
|
key={lecture.id}
|
||||||
className={styles.card}
|
to={`/lecture/${lecture.id}`}
|
||||||
>
|
className={styles.card}
|
||||||
<div className={styles.thumbnail} />
|
>
|
||||||
<div>{lecture.title}</div>
|
<div className={styles.thumbnail} />
|
||||||
</Link>
|
<div>{lecture.title}</div>
|
||||||
))}
|
</Link>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className={styles.empty}>수강중인 강의가 없습니다.</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
@ -24,14 +24,15 @@ export default function QuestionListPage() {
|
|||||||
title="수강신청관리"
|
title="수강신청관리"
|
||||||
canCreate={false}
|
canCreate={false}
|
||||||
>
|
>
|
||||||
{lectures.map?.((lecture) => (
|
{lectures.length &&
|
||||||
<LectureEnroll
|
lectures.map?.((lecture) => (
|
||||||
key={`${lecture.id}`}
|
<LectureEnroll
|
||||||
enrollid={lecture.id}
|
key={`${lecture.id}`}
|
||||||
userName={lecture.userName}
|
enrollid={lecture.id}
|
||||||
onDelete={handleDelete}
|
userName={lecture.userName}
|
||||||
/>
|
onDelete={handleDelete}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
</ArticleBoard>
|
</ArticleBoard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { LiveRoom } from '../../components/LiveRoom';
|
import { LiveRoom } from '../../components/LiveRoom';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { Suspense, useCallback, useEffect, useState } from 'react';
|
||||||
import { LiveKitRoom } from '@livekit/components-react';
|
import { LiveKitRoom } from '@livekit/components-react';
|
||||||
import instance from '../../utils/axios/instance';
|
import instance from '../../utils/axios/instance';
|
||||||
import { API_URL, ROOM_URL } from '../../constants';
|
import { API_URL, ROOM_URL } from '../../constants';
|
||||||
@ -43,7 +43,9 @@ export default function LivePage() {
|
|||||||
}, 500);
|
}, 500);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LiveRoom />
|
<Suspense fallback={<LoadingIndicator fill />}>
|
||||||
|
<LiveRoom />
|
||||||
|
</Suspense>
|
||||||
</LiveKitRoom>
|
</LiveKitRoom>
|
||||||
) : (
|
) : (
|
||||||
<LoadingIndicator fill />
|
<LoadingIndicator fill />
|
||||||
|
@ -22,9 +22,15 @@ export default function LoginPage() {
|
|||||||
const id = idRef.current.value;
|
const id = idRef.current.value;
|
||||||
const password = passwordRef.current.value;
|
const password = passwordRef.current.value;
|
||||||
|
|
||||||
login(id, password).then(() => {
|
login(id, password)
|
||||||
navigate('/', { replace: true });
|
.then(() => {
|
||||||
});
|
navigate('/', { replace: true });
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
alert('아이디 또는 비밀번호를 다시 확인해주세요.');
|
||||||
|
passwordRef.current.value = '';
|
||||||
|
idRef.current.focus();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -15,16 +15,17 @@ export default function NoticeListPage() {
|
|||||||
title="공지사항"
|
title="공지사항"
|
||||||
canCreate={userType === 'teacher'}
|
canCreate={userType === 'teacher'}
|
||||||
>
|
>
|
||||||
{notices.map?.((notice) => (
|
{notices.length &&
|
||||||
<ArticleLink
|
notices.map?.((notice) => (
|
||||||
key={`${notice.id}`}
|
<ArticleLink
|
||||||
title={notice.title}
|
key={`${notice.id}`}
|
||||||
//Todo: createdAt을 이용하여 날짜 표시했는데 Q&A에서 나오는 날짜 형식이랑 공지사항에서 나오는 날짜 형식이랑 달라서 수정해야함.
|
title={notice.title}
|
||||||
// + Q&A 글쓰기 버튼이 너무 커서 글 밀어내는 느낌 있음.
|
//Todo: createdAt을 이용하여 날짜 표시했는데 Q&A에서 나오는 날짜 형식이랑 공지사항에서 나오는 날짜 형식이랑 달라서 수정해야함.
|
||||||
sub={`${notice.createdAt[0]}. ${notice.createdAt[1]}. ${notice.createdAt[2]}. ${notice.createdAt[3]}:${notice.createdAt[4]}`}
|
// + Q&A 글쓰기 버튼이 너무 커서 글 밀어내는 느낌 있음.
|
||||||
to={`${notice.id}`}
|
sub={`${notice.createdAt[0]}. ${notice.createdAt[1]}. ${notice.createdAt[2]}. ${notice.createdAt[3]}:${notice.createdAt[4]}`}
|
||||||
/>
|
to={`${notice.id}`}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
</ArticleBoard>
|
</ArticleBoard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -15,14 +15,15 @@ export default function QuestionListPage() {
|
|||||||
title="Q&A"
|
title="Q&A"
|
||||||
canCreate={userType === 'student'}
|
canCreate={userType === 'student'}
|
||||||
>
|
>
|
||||||
{questions.map?.((question) => (
|
{questions.length &&
|
||||||
<ArticleLink
|
questions.map?.((question) => (
|
||||||
key={`${question.title}${question.createtAt}`}
|
<ArticleLink
|
||||||
title={question.title}
|
key={`${question.title}${question.createtAt}`}
|
||||||
sub={`${new Date(question.createtAt).toLocaleDateString()} ${new Date(question.createtAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`}
|
title={question.title}
|
||||||
to={`${question.id}`}
|
sub={`${new Date(question.createtAt).toLocaleDateString()} ${new Date(question.createtAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`}
|
||||||
/>
|
to={`${question.id}`}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
</ArticleBoard>
|
</ArticleBoard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -15,13 +15,14 @@ export default function QuizsetListPage() {
|
|||||||
title="퀴즈 목록"
|
title="퀴즈 목록"
|
||||||
canCreate={true}
|
canCreate={true}
|
||||||
>
|
>
|
||||||
{quizsets.map?.((quizset) => (
|
{quizsets.length &&
|
||||||
<ArticleLink
|
quizsets.map?.((quizset) => (
|
||||||
key={`${quizset.quizSetId}`}
|
<ArticleLink
|
||||||
title={quizset.title}
|
key={`${quizset.quizSetId}`}
|
||||||
to={`${quizset.quizSetId}`}
|
title={quizset.title}
|
||||||
/>
|
to={`${quizset.quizSetId}`}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
</ArticleBoard>
|
</ArticleBoard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import styles from './StudentHomePage.module.css';
|
||||||
import { ClassCard } from '../../components/ClassCard';
|
import { ClassCard } from '../../components/ClassCard';
|
||||||
import { ClassGrid } from '../../components/ClassGrid';
|
import { ClassGrid } from '../../components/ClassGrid';
|
||||||
import { MaxWidthLayout } from '../../components/Layout';
|
import { MaxWidthLayout } from '../../components/Layout';
|
||||||
@ -7,6 +8,7 @@ import { useMyLectures } from '../../hooks/api/useMyLectures';
|
|||||||
export default function StudentHomePage() {
|
export default function StudentHomePage() {
|
||||||
const { data: myLectures } = useMyLectures();
|
const { data: myLectures } = useMyLectures();
|
||||||
const onGoingClasses = myLectures?.data ?? [];
|
const onGoingClasses = myLectures?.data ?? [];
|
||||||
|
const hasOnGoingClasses = onGoingClasses.length > 0;
|
||||||
|
|
||||||
const { data: allLectures } = useLectures();
|
const { data: allLectures } = useLectures();
|
||||||
const allClasses = allLectures?.data ?? [];
|
const allClasses = allLectures?.data ?? [];
|
||||||
@ -14,15 +16,19 @@ export default function StudentHomePage() {
|
|||||||
return (
|
return (
|
||||||
<MaxWidthLayout>
|
<MaxWidthLayout>
|
||||||
<ClassGrid title="수강중인 강의">
|
<ClassGrid title="수강중인 강의">
|
||||||
{onGoingClasses.map?.((lecture) => (
|
{hasOnGoingClasses ? (
|
||||||
<ClassCard
|
onGoingClasses.map?.((lecture) => (
|
||||||
key={lecture.id}
|
<ClassCard
|
||||||
path={`/lecture/${lecture.id}`}
|
key={lecture.id}
|
||||||
img={lecture.image}
|
path={`/lecture/${lecture.id}`}
|
||||||
>
|
img={lecture.image}
|
||||||
{lecture.title}
|
>
|
||||||
</ClassCard>
|
{lecture.title}
|
||||||
))}
|
</ClassCard>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className={styles.msg}>수강중인 강의가 없어요.</div>
|
||||||
|
)}
|
||||||
</ClassGrid>
|
</ClassGrid>
|
||||||
<ClassGrid title="전체 강의">
|
<ClassGrid title="전체 강의">
|
||||||
{allClasses.map?.((lecture) => (
|
{allClasses.map?.((lecture) => (
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
.msg {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
color: var(--text-color-tertiary);
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
@ -14,16 +14,17 @@ export default function StudentListPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ArticleBoard title="수강생 관리">
|
<ArticleBoard title="수강생 관리">
|
||||||
{students.map?.((student) => {
|
{students.length &&
|
||||||
return (
|
students.map?.((student) => {
|
||||||
<ArticleLink
|
return (
|
||||||
key={`${student.name}${student.sub}`}
|
<ArticleLink
|
||||||
title={student.name}
|
key={`${student.name}${student.sub}`}
|
||||||
sub={`퀴즈 점수: ${student.quizScore}`}
|
title={student.name}
|
||||||
to={`${student.id}`}
|
sub={`퀴즈 점수: ${student.quizScore}`}
|
||||||
/>
|
to={`${student.id}`}
|
||||||
);
|
/>
|
||||||
})}
|
);
|
||||||
|
})}
|
||||||
</ArticleBoard>
|
</ArticleBoard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
|
width: 295px;
|
||||||
|
height: 220px;
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
stroke: var(--text-color);
|
stroke: var(--text-color);
|
||||||
@ -19,3 +21,14 @@
|
|||||||
background-color: var(--background-secondary);
|
background-color: var(--background-secondary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.msg {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
color: var(--text-color-tertiary);
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
import ArticlePreview from '../../components/Article/ArticlePreview/ArticlePreview';
|
|
||||||
import styles from './TeacherLectureDetailPage.module.css';
|
|
||||||
import { useNotices } from '../../hooks/api/useNotices';
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import { useQnas } from '../../hooks/api/useQnas';
|
|
||||||
|
|
||||||
export default function TeacherLectureDetailPage() {
|
|
||||||
const { lectureId } = useParams();
|
|
||||||
const { data: noticesData } = useNotices(lectureId);
|
|
||||||
const notices = noticesData?.data;
|
|
||||||
const { data: qnasData } = useQnas(lectureId);
|
|
||||||
const questions = qnasData?.data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className={styles.previews}>
|
|
||||||
{/* FIXME: 밑에 ArticlePreview 바꿔야함. 공지사항 Q&A 커리큘럼 으로 나눠서 작성할 수 있게 바꾸고 링크 상위 3개만 받고 링크 줄 수 있게 할지 말지. 이거 바꾸면 LearningLectureDetailPage도 똑같이 바꾸면 될듯*/}
|
|
||||||
<ArticlePreview
|
|
||||||
to="notice"
|
|
||||||
title="공지사항"
|
|
||||||
contents={notices}
|
|
||||||
/>
|
|
||||||
<ArticlePreview
|
|
||||||
to="qna"
|
|
||||||
title="Q&A"
|
|
||||||
contents={questions}
|
|
||||||
/>
|
|
||||||
<ArticlePreview title="커리큘럼" />
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
.previews {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 40px;
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
export { default } from './TeacherLectureDetailPage';
|
|
@ -16,17 +16,18 @@ export default function TeacherNoticeListPage() {
|
|||||||
title="공지사항"
|
title="공지사항"
|
||||||
canCreate={true}
|
canCreate={true}
|
||||||
>
|
>
|
||||||
{notices.map?.((notice) => {
|
{notices.length &&
|
||||||
if (notice.sub && notice.title) {
|
notices.map?.((notice) => {
|
||||||
return (
|
if (notice.sub && notice.title) {
|
||||||
<ArticleLink
|
return (
|
||||||
key={`${notice.title}${notice.sub}`}
|
<ArticleLink
|
||||||
title={notice.title}
|
key={`${notice.title}${notice.sub}`}
|
||||||
sub={notice.sub}
|
title={notice.title}
|
||||||
/>
|
sub={notice.sub}
|
||||||
);
|
/>
|
||||||
}
|
);
|
||||||
})}
|
}
|
||||||
|
})}
|
||||||
</ArticleBoard>
|
</ArticleBoard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user