Merge branch 'frontend' into 'fe/useQnaEdit'
# Conflicts: # frontend/src/Router.jsx
This commit is contained in:
commit
777495ae4b
108
frontend/package-lock.json
generated
108
frontend/package-lock.json
generated
@ -11,6 +11,7 @@
|
||||
"@stomp/stompjs": "^7.0.0",
|
||||
"@tanstack/react-query": "^5.49.2",
|
||||
"axios": "^1.7.2",
|
||||
"livekit-client": "^2.4.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.24.1",
|
||||
@ -447,6 +448,12 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protobuf": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz",
|
||||
"integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==",
|
||||
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
@ -1002,6 +1009,15 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@livekit/protocol": {
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.19.1.tgz",
|
||||
"integrity": "sha512-PQYIuqRv++fRik9tKulJ0C0tT5O4cNviBA7OxwLTCBFDxJpve8ua8/JZ+nK+7r4j2KbLfVjsJYop9wcTCgRn7Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^1.7.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@ -3177,6 +3193,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
||||
@ -4250,6 +4275,22 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/livekit-client": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.4.1.tgz",
|
||||
"integrity": "sha512-GZwYsrrv6QKyfM3TCxgFoNQkSiFZV9cJbiW2X2N4fSt7KxeHwy9/tVGUr79ak6SIa0uIa8RuG1ELHzsrutsCow==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@livekit/protocol": "1.19.1",
|
||||
"events": "^3.3.0",
|
||||
"loglevel": "^1.8.0",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"ts-debounce": "^4.0.0",
|
||||
"tslib": "2.6.3",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"webrtc-adapter": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/local-pkg": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz",
|
||||
@ -4290,6 +4331,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loglevel": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.1.tgz",
|
||||
"integrity": "sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/loglevel"
|
||||
}
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@ -5185,6 +5239,16 @@
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-array-concat": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz",
|
||||
@ -5231,6 +5295,21 @@
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sdp": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz",
|
||||
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sdp-transform": {
|
||||
"version": "2.14.2",
|
||||
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.2.tgz",
|
||||
"integrity": "sha512-icY6jVao7MfKCieyo1AyxFYm1baiM+fA00qW/KrNNVlkxHAd34riEKuEkUe4bBb3gJwLJZM+xT60Yj1QL8rHiA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"sdp-verify": "checker.js"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
@ -5587,11 +5666,16 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-debounce": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-debounce/-/ts-debounce-4.0.0.tgz",
|
||||
"integrity": "sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
@ -5707,6 +5791,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/typed-emitter": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz",
|
||||
"integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"rxjs": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ufo": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz",
|
||||
@ -5940,6 +6033,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webrtc-adapter": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.1.tgz",
|
||||
"integrity": "sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"sdp": "^3.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0",
|
||||
"npm": ">=3.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
@ -16,6 +16,7 @@
|
||||
"@stomp/stompjs": "^7.0.0",
|
||||
"@tanstack/react-query": "^5.49.2",
|
||||
"axios": "^1.7.2",
|
||||
"livekit-client": "^2.4.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.24.1",
|
||||
|
@ -5,8 +5,8 @@ import HomePage from './pages/HomePage';
|
||||
import NotFoundPage from './pages/NotFoundPage';
|
||||
import { lazy } from 'react';
|
||||
import MyPageLayout from './components/Layout/MyPageLayout';
|
||||
import { LiveLayout } from './components/Layout';
|
||||
|
||||
const LiveLayout = lazy(async () => await import('./components/Layout/LiveLayout'));
|
||||
const LivePage = lazy(async () => await import('./pages/LivePage'));
|
||||
const LectureLayout = lazy(async () => await import('./components/Layout/LectureLayout'));
|
||||
const LearningLectureDetailPage = lazy(async () => await import('./pages/LearningLectureDetailPage'));
|
||||
@ -26,6 +26,10 @@ const PasswordChangePage = lazy(async () => await import('./pages/PasswordChange
|
||||
const LearningLecturesPage = lazy(async () => await import('./pages/LearningLecturesPage'));
|
||||
const LectureCreatePage = lazy(async () => await import('./pages/LectureCreatePage'));
|
||||
const EditQuestionPage = lazy(async () => await import('./pages/EditQuestionPage'));
|
||||
const LectureEditPage = lazy(async () => await import('./pages/LectureEditPage'));
|
||||
const QuizsetListPage = lazy(async () => await import('./pages/QuizsetListPage'));
|
||||
const QuizsetWritePage = lazy(async () => await import('./pages/QuizsetWritePage'));
|
||||
const QuizsetDetailPage = lazy(async () => await import('./pages/QuizsetDetailPage'));
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@ -55,6 +59,10 @@ const router = createBrowserRouter([
|
||||
path: 'lecture/:lectureId/info',
|
||||
element: <LectureInfoPage />,
|
||||
},
|
||||
{
|
||||
path: 'lecture/:lectureId/edit',
|
||||
element: <LectureEditPage />,
|
||||
},
|
||||
{
|
||||
path: 'lecture/:lectureId',
|
||||
element: <LectureLayout />,
|
||||
@ -63,6 +71,10 @@ const router = createBrowserRouter([
|
||||
index: true,
|
||||
element: <LearningLectureDetailPage />,
|
||||
},
|
||||
{
|
||||
path: 'edit',
|
||||
element: <LectureEditPage />,
|
||||
},
|
||||
{
|
||||
path: 'notice',
|
||||
children: [
|
||||
@ -119,6 +131,23 @@ const router = createBrowserRouter([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'quiz',
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <QuizsetListPage />,
|
||||
},
|
||||
{
|
||||
path: 'write',
|
||||
element: <QuizsetWritePage />,
|
||||
},
|
||||
{
|
||||
path: ':quizsetId',
|
||||
element: <QuizsetDetailPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -1,7 +1,16 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import styles from './Header.module.css';
|
||||
import useBoundStore from '../../store';
|
||||
import { useAuth } from '../../hooks/api/useAuth';
|
||||
|
||||
export default function Header() {
|
||||
const navigate = useNavigate();
|
||||
const userType = useBoundStore((state) => state.userType);
|
||||
const { logout } = useAuth();
|
||||
const handleClick = () => {
|
||||
logout().then(navigate('/'));
|
||||
};
|
||||
|
||||
return (
|
||||
<header className={styles.header}>
|
||||
<nav className={styles.nav}>
|
||||
@ -24,16 +33,25 @@ export default function Header() {
|
||||
<Link to={'/'}>수강중인 강의</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to={'/'}>내 학습</Link>
|
||||
<Link to={'/live/1'}>live</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<ul className={styles.group}>
|
||||
<li>
|
||||
<Link to={'user/my'}>마이페이지</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to={'/auth/login'}>로그인</Link>
|
||||
</li>
|
||||
{userType && (
|
||||
<>
|
||||
<li>
|
||||
<Link to={'user/my'}>마이페이지</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link onClick={handleClick}>로그아웃</Link>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
{!userType && (
|
||||
<li>
|
||||
<Link to={'/auth/login'}>로그인</Link>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
@ -5,14 +5,31 @@ import MaxWidthLayout from './MaxWidthLayout';
|
||||
import { Suspense } from 'react';
|
||||
import useBoundStore from '../../store';
|
||||
import { useLectureInfo } from '../../hooks/api/useLectureInfo';
|
||||
import LoadingIndicator from '../LoadingIndicator.jsx/LoadingIndicator';
|
||||
import { useLectureDelete } from '../../hooks/api/useLectureDelete';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export default function LectureLayout() {
|
||||
const { lectureId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { lectureDelete } = useLectureDelete();
|
||||
const { data } = useLectureInfo(lectureId);
|
||||
const lecture = data?.data;
|
||||
console.log(lecture);
|
||||
const userType = useBoundStore((state) => state.userType);
|
||||
|
||||
const handleDelete = () => {
|
||||
lectureDelete(lectureId);
|
||||
navigate('..');
|
||||
};
|
||||
const lectureData = {
|
||||
title: lecture.title,
|
||||
description: lecture.description,
|
||||
plan: lecture.plan,
|
||||
startDate: lecture.startDate,
|
||||
endDate: lecture.endDate,
|
||||
time: lecture.time,
|
||||
};
|
||||
console.log(lectureData);
|
||||
return (
|
||||
<>
|
||||
<LectureHeader
|
||||
@ -42,6 +59,13 @@ export default function LectureLayout() {
|
||||
name="수강생"
|
||||
sub="총 12명"
|
||||
/>
|
||||
<SideLink
|
||||
to={'edit'}
|
||||
state={lectureData}
|
||||
>
|
||||
강의 정보 수정
|
||||
</SideLink>
|
||||
<button onClick={handleDelete}>강의 삭제</button>
|
||||
</SideBar>
|
||||
)}
|
||||
{userType === 'student' && (
|
||||
@ -58,7 +82,7 @@ export default function LectureLayout() {
|
||||
)}
|
||||
</aside>
|
||||
<main>
|
||||
<Suspense fallback={<div>loading</div>}>
|
||||
<Suspense fallback={<LoadingIndicator fill />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</main>
|
||||
|
@ -1,13 +1,17 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import styles from './LiveLayout.module.css';
|
||||
import { LiveHeader } from '../LiveHeader';
|
||||
import { Suspense } from 'react';
|
||||
import LoadingIndicator from '../LoadingIndicator.jsx/LoadingIndicator';
|
||||
|
||||
export default function LiveLayout() {
|
||||
return (
|
||||
<>
|
||||
<LiveHeader />
|
||||
<div className={styles.wrapper}>
|
||||
<Outlet />
|
||||
<Suspense fallback={<LoadingIndicator fill />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -4,7 +4,7 @@
|
||||
align-items: stretch;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
padding-top: 64px;
|
||||
padding-top: 48px;
|
||||
box-sizing: border-box;
|
||||
|
||||
& > main {
|
||||
|
@ -3,6 +3,7 @@ import MaxWidthLayout from '../../components/Layout/MaxWidthLayout';
|
||||
import SideBar from '../../components/SideBar/SideBar';
|
||||
import SideLink from '../../components/SideBar/SideLink';
|
||||
import { Suspense } from 'react';
|
||||
import LoadingIndicator from '../LoadingIndicator.jsx/LoadingIndicator';
|
||||
|
||||
export default function MyPageLayout() {
|
||||
return (
|
||||
@ -21,8 +22,7 @@ export default function MyPageLayout() {
|
||||
</SideBar>
|
||||
</aside>
|
||||
<main>
|
||||
{/* TODO: 로딩 컴포넌트 추가 */}
|
||||
<Suspense fallback={<div>loading</div>}>
|
||||
<Suspense fallback={<LoadingIndicator fill />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</main>
|
||||
|
@ -3,6 +3,7 @@ import { Footer } from '../Footer';
|
||||
import { Header } from '../Header';
|
||||
import styles from './PageLayout.module.css';
|
||||
import { Suspense } from 'react';
|
||||
import LoadingIndicator from '../LoadingIndicator.jsx/LoadingIndicator';
|
||||
|
||||
export default function PageLayout() {
|
||||
return (
|
||||
@ -10,7 +11,7 @@ export default function PageLayout() {
|
||||
<Header />
|
||||
<div className={styles.body}>
|
||||
<div className={styles.contents}>
|
||||
<Suspense fallback={<div>loading</div>}>
|
||||
<Suspense fallback={<LoadingIndicator fill />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</div>
|
||||
|
131
frontend/src/components/LectureForm/LectureForm.jsx
Normal file
131
frontend/src/components/LectureForm/LectureForm.jsx
Normal file
@ -0,0 +1,131 @@
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styles from './LectureForm.module.css';
|
||||
import EditIcon from '/src/assets/icons/edit.svg?react';
|
||||
import BackIcon from '/src/assets/icons/back.svg?react';
|
||||
|
||||
export default function LectureForm({ title, topic, to, initialValues = {}, onSubmit, onCreate = false }) {
|
||||
// TODO: 디자인 필요, 필요시 useState로 수정하고 버튼 비활성화 기능 추가
|
||||
const titleRef = useRef('');
|
||||
const descriptionRef = useRef('');
|
||||
const planRef = useRef('');
|
||||
const startDateRef = useRef('');
|
||||
const endDateRef = useRef('');
|
||||
const timeRef = useRef('');
|
||||
const imageFileRef = useRef(null);
|
||||
|
||||
// 초기 값 설정
|
||||
useEffect(() => {
|
||||
if (initialValues.title) titleRef.current.value = initialValues.title;
|
||||
if (initialValues.description) descriptionRef.current.value = initialValues.description;
|
||||
if (initialValues.plan) planRef.current.value = initialValues.plan;
|
||||
if (initialValues.startDate)
|
||||
startDateRef.current.value = new Date(initialValues.startDate).toISOString().split('T')[0];
|
||||
if (initialValues.endDate) endDateRef.current.value = new Date(initialValues.endDate).toISOString().split('T')[0];
|
||||
if (initialValues.time) timeRef.current.value = initialValues.time;
|
||||
}, [initialValues]);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const lectureObject = {
|
||||
title: titleRef.current.value,
|
||||
description: descriptionRef.current.value,
|
||||
plan: planRef.current.value,
|
||||
startDate: new Date(startDateRef.current.value).toISOString(),
|
||||
endDate: new Date(endDateRef.current.value).toISOString(),
|
||||
time: timeRef.current.value,
|
||||
};
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('lectureCreateRequest', new Blob([JSON.stringify(lectureObject)], { type: 'application/json' }));
|
||||
|
||||
const imageFile = (imageFileRef.current && imageFileRef.current.files[0]) ?? null;
|
||||
if (imageFile) {
|
||||
formData.append('image', imageFile);
|
||||
}
|
||||
|
||||
onSubmit(lectureObject, imageFile);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.createClass}>
|
||||
<header className={styles.header}>
|
||||
<Link
|
||||
to={to}
|
||||
className={styles.goBack}
|
||||
>
|
||||
<BackIcon />
|
||||
<span>{title}</span>
|
||||
</Link>
|
||||
<div className={styles.title}>{topic}</div>
|
||||
</header>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className={styles.inputField}>
|
||||
<label className={styles.label}>강의명</label>
|
||||
<input
|
||||
className={styles.input}
|
||||
ref={titleRef}
|
||||
type="text"
|
||||
placeholder="강의명을 입력하세요"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.inputField}>
|
||||
<label className={styles.label}>설명</label>
|
||||
<textarea
|
||||
ref={descriptionRef}
|
||||
className={styles.textarea}
|
||||
placeholder="강의에 대한 설명을 입력하세요"
|
||||
></textarea>
|
||||
</div>
|
||||
<div className={styles.inputField}>
|
||||
<label className={styles.label}>강의 계획</label>
|
||||
<textarea
|
||||
ref={planRef}
|
||||
className={styles.textarea}
|
||||
placeholder="강의 계획을 입력하세요"
|
||||
></textarea>
|
||||
</div>
|
||||
<div className={styles.inputField}>
|
||||
<label className={styles.label}>강의 기간</label>
|
||||
<input
|
||||
className={styles.input}
|
||||
ref={startDateRef}
|
||||
type="date"
|
||||
/>
|
||||
<input
|
||||
className={styles.input}
|
||||
ref={endDateRef}
|
||||
type="date"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.inputField}>
|
||||
<label className={styles.label}>수업 시간</label>
|
||||
<input
|
||||
type="text"
|
||||
ref={timeRef}
|
||||
className={styles.input}
|
||||
placeholder="실제 강의 진행 시간을 입력하세요"
|
||||
/>
|
||||
</div>
|
||||
{onCreate && (
|
||||
<div className={styles.inputField}>
|
||||
<label className={styles.label}>수업 이미지</label>
|
||||
<input
|
||||
type="file"
|
||||
ref={imageFileRef}
|
||||
accept=".png, .jpg, .jpeg"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
className={styles.button}
|
||||
>
|
||||
<EditIcon />
|
||||
<div>제출</div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -8,6 +8,30 @@
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.goBack {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
font-weight: 400;
|
||||
color: var(--text-color-secondary);
|
||||
stroke: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32px;
|
||||
line-height: 1.2;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.inputField {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -52,18 +76,17 @@
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 16px 24px;
|
||||
border-radius: 8px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--primary-color);
|
||||
background-color: var(--primary-color);
|
||||
color: var(--on-primary);
|
||||
gap: 8px;
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
.buttonText {
|
||||
stroke: var(--on-primary);
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
font-weight: 700;
|
||||
align-self: end;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
1
frontend/src/components/LectureForm/index.js
Normal file
1
frontend/src/components/LectureForm/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default as LectureForm } from './LectureForm';
|
22
frontend/src/components/LiveAudio/LiveAudio.jsx
Normal file
22
frontend/src/components/LiveAudio/LiveAudio.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export default function LiveAudio({ track }) {
|
||||
const audioRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (audioRef.current) {
|
||||
track.attach(audioRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
track.detach();
|
||||
};
|
||||
}, [track]);
|
||||
|
||||
return (
|
||||
<audio
|
||||
ref={audioRef}
|
||||
id={track.sid}
|
||||
/>
|
||||
);
|
||||
}
|
1
frontend/src/components/LiveAudio/index.js
Normal file
1
frontend/src/components/LiveAudio/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default as LiveAudio } from './LiveAudio';
|
@ -3,7 +3,7 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
height: 48px;
|
||||
background-color: var(--background);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
z-index: 9999;
|
||||
@ -27,7 +27,7 @@
|
||||
|
||||
& > h1,
|
||||
& > h2 {
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
font-weight: 700;
|
||||
color: var(--text-color);
|
||||
@ -35,6 +35,7 @@
|
||||
}
|
||||
|
||||
& > div {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
46
frontend/src/components/LiveRoom/LiveRoom.jsx
Normal file
46
frontend/src/components/LiveRoom/LiveRoom.jsx
Normal file
@ -0,0 +1,46 @@
|
||||
import styles from './LiveRoom.module.css';
|
||||
import ChatRoom from '../ChatRoom/ChatRoom';
|
||||
import { LiveAudio } from '../LiveAudio';
|
||||
import { LiveVideo } from '../LiveVideo';
|
||||
import LoadingIndicator from '../LoadingIndicator.jsx/LoadingIndicator';
|
||||
|
||||
export default function LiveRoom({ room, localTrack, remoteTracks, leaveRoom, mainTrack }) {
|
||||
return (
|
||||
<>
|
||||
<main>
|
||||
{room ? (
|
||||
<>
|
||||
<div className={styles.videoWrapper}>
|
||||
{localTrack && (
|
||||
<LiveVideo
|
||||
track={localTrack}
|
||||
identity="나"
|
||||
/>
|
||||
)}
|
||||
{remoteTracks.map((track) =>
|
||||
track.trackPublication.kind === 'video' ? (
|
||||
<LiveVideo
|
||||
key={track.trackPublication.trackSid}
|
||||
track={track.trackPublication.videoTrack}
|
||||
/>
|
||||
) : (
|
||||
<LiveAudio
|
||||
key={track.trackPublication.trackSid}
|
||||
track={track.trackPublication.audioTrack}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{mainTrack && <LiveVideo track={mainTrack} />}
|
||||
</>
|
||||
) : (
|
||||
<LoadingIndicator fill />
|
||||
)}
|
||||
</main>
|
||||
<aside>
|
||||
<ChatRoom />
|
||||
<button onClick={leaveRoom}>나가기</button>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
27
frontend/src/components/LiveRoom/LiveRoom.module.css
Normal file
27
frontend/src/components/LiveRoom/LiveRoom.module.css
Normal file
@ -0,0 +1,27 @@
|
||||
.videoWrapper {
|
||||
flex-grow: 0;
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
height: 80px;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
& > audio {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--text-color-tertiary);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.myVideo {
|
||||
border-radius: 12px;
|
||||
border: 2px solid var(--border-color);
|
||||
}
|
1
frontend/src/components/LiveRoom/index.js
Normal file
1
frontend/src/components/LiveRoom/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default as LiveRoom } from './LiveRoom';
|
24
frontend/src/components/LiveVideo/LiveVideo.jsx
Normal file
24
frontend/src/components/LiveVideo/LiveVideo.jsx
Normal file
@ -0,0 +1,24 @@
|
||||
import styles from './LiveVideo.module.css';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export default function LiveVideo({ track }) {
|
||||
const videoRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (videoRef.current) {
|
||||
track.attach(videoRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
track.detach();
|
||||
};
|
||||
}, [track]);
|
||||
|
||||
return (
|
||||
<video
|
||||
ref={videoRef}
|
||||
id={track?.sid}
|
||||
className={styles.video}
|
||||
/>
|
||||
);
|
||||
}
|
36
frontend/src/components/LiveVideo/LiveVideo.module.css
Normal file
36
frontend/src/components/LiveVideo/LiveVideo.module.css
Normal file
@ -0,0 +1,36 @@
|
||||
.wrapper {
|
||||
width: fit-content;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
& > p {
|
||||
z-index: 99;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
background-color: var(--whiteOpacity600);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--text-color);
|
||||
font-weight: bold;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
&:hover > p {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.video {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
1
frontend/src/components/LiveVideo/index.js
Normal file
1
frontend/src/components/LiveVideo/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default as LiveVideo } from './LiveVideo';
|
@ -0,0 +1,11 @@
|
||||
import styles from './LoadingIndicator.module.css';
|
||||
|
||||
export default function LoadingIndicator({ fill = false }) {
|
||||
return fill ? (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.indicator} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.indicator} />
|
||||
);
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(720deg);
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border: 5px solid var(--background-secondary);
|
||||
border-top-color: var(--primary-color);
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
animation: spin 2s cubic-bezier(0.645, 0.045, 0.355, 1) infinite;
|
||||
}
|
86
frontend/src/components/QuizForm/QuizCard.jsx
Normal file
86
frontend/src/components/QuizForm/QuizCard.jsx
Normal file
@ -0,0 +1,86 @@
|
||||
import { useState } from 'react';
|
||||
import styles from './QuizCard.module.css';
|
||||
|
||||
export default function QuizCard({ quiz, index, updateQuiz }) {
|
||||
const [question, setQuestion] = useState(quiz.question || '');
|
||||
const [answer, setAnswer] = useState(quiz.answer || '');
|
||||
const [choices, setChoices] = useState(quiz.choices || []);
|
||||
|
||||
const handleChoiceChange = (num, content) => {
|
||||
const updatedChoices = choices.map((choice) => (choice.num === num ? { ...choice, content } : choice));
|
||||
setChoices(updatedChoices);
|
||||
updateQuiz(index, { question, answer, choices: updatedChoices });
|
||||
};
|
||||
|
||||
const handleAddChoice = () => {
|
||||
if (choices.length < 4) {
|
||||
const newChoice = { num: choices.length + 1, content: '' };
|
||||
const updatedChoices = [...choices, newChoice];
|
||||
setChoices(updatedChoices);
|
||||
updateQuiz(index, { question, answer, choices: updatedChoices });
|
||||
}
|
||||
};
|
||||
|
||||
const handlePopChoice = () => {
|
||||
if (choices.length > 0) {
|
||||
const updatedChoices = choices.slice(0, -1);
|
||||
setChoices(updatedChoices);
|
||||
updateQuiz(index, { question, answer, choices: updatedChoices });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.card}>
|
||||
<label>질문</label>
|
||||
<input
|
||||
type="text"
|
||||
value={question}
|
||||
onChange={(e) => {
|
||||
setQuestion(e.target.value);
|
||||
updateQuiz(index, { question: e.target.value, answer, choices });
|
||||
}}
|
||||
placeholder="질문 내용을 입력하세요"
|
||||
/>
|
||||
<label>정답</label>
|
||||
<input
|
||||
type="text"
|
||||
value={answer}
|
||||
onChange={(e) => {
|
||||
setAnswer(e.target.value);
|
||||
updateQuiz(index, { question, answer: e.target.value, choices });
|
||||
}}
|
||||
placeholder="정답을 입력하세요"
|
||||
/>
|
||||
<div>
|
||||
<span>Tip: 선택지를 넣지 않는다면 단답형 문제가 됩니다</span>
|
||||
</div>
|
||||
<div className={styles.buttonsWrapper}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddChoice}
|
||||
className={styles.button}
|
||||
>
|
||||
선택지 추가하기
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePopChoice}
|
||||
className={styles.removeButton}
|
||||
>
|
||||
선택지 줄이기
|
||||
</button>
|
||||
</div>
|
||||
{choices.map?.((choice, idx) => (
|
||||
<div key={idx}>
|
||||
<label>선택지 {choice.num} : </label>
|
||||
<input
|
||||
type="text"
|
||||
value={choice.content}
|
||||
onChange={(e) => handleChoiceChange(choice.num, e.target.value)}
|
||||
placeholder={`Choice ${choice.num}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
47
frontend/src/components/QuizForm/QuizCard.module.css
Normal file
47
frontend/src/components/QuizForm/QuizCard.module.css
Normal file
@ -0,0 +1,47 @@
|
||||
.card {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 10px 20px;
|
||||
width: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.buttonsWrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.removeButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--accent-color);
|
||||
background-color: var(--accent-color);
|
||||
color: var(--on-primary);
|
||||
stroke: var(--on-primary);
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
font-weight: 700;
|
||||
align-self: end;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--primary-color);
|
||||
background-color: var(--primary-color);
|
||||
color: var(--on-primary);
|
||||
stroke: var(--on-primary);
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
font-weight: 700;
|
||||
align-self: end;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
81
frontend/src/components/QuizForm/QuizsetForm.jsx
Normal file
81
frontend/src/components/QuizForm/QuizsetForm.jsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { useState } from 'react';
|
||||
import QuizCard from './QuizCard';
|
||||
import styles from './QuizsetForm.module.css';
|
||||
import EditIcon from '/src/assets/icons/edit.svg?react';
|
||||
import BackIcon from '/src/assets/icons/back.svg?react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function QuizsetForm({ headerTitle, topic, to, onSubmit }) {
|
||||
// TODO: 디자인 만들기 및 스타일 적용
|
||||
const [title, setTitle] = useState('');
|
||||
const [quizzes, setQuizzes] = useState([]);
|
||||
const [imageFile, setImageFile] = useState(null);
|
||||
|
||||
const handleAddQuiz = () => {
|
||||
setQuizzes([...quizzes, { question: '', answer: '', choices: [] }]);
|
||||
};
|
||||
|
||||
const updateQuiz = (index, updatedQuiz) => {
|
||||
const updatedQuizzes = quizzes.map((quiz, i) => (i === index ? updatedQuiz : quiz));
|
||||
setQuizzes(updatedQuizzes);
|
||||
};
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
setImageFile(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.quizsetForm}>
|
||||
<header className={styles.header}>
|
||||
<Link
|
||||
to={to}
|
||||
className={styles.goBack}
|
||||
>
|
||||
<BackIcon />
|
||||
<span>{headerTitle}</span>
|
||||
</Link>
|
||||
<div className={styles.title}>{topic}</div>
|
||||
</header>
|
||||
<form
|
||||
className={styles.form}
|
||||
onSubmit={(e) => onSubmit(e, title, quizzes, imageFile)}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="퀴즈셋 제목을 입력해주세요"
|
||||
/>
|
||||
{quizzes.map((quiz, index) => (
|
||||
<QuizCard
|
||||
key={index}
|
||||
quiz={quiz}
|
||||
index={index}
|
||||
updateQuiz={updateQuiz}
|
||||
/>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddQuiz}
|
||||
className={styles.button}
|
||||
>
|
||||
퀴즈 추가하기
|
||||
</button>
|
||||
<label>퀴즈 이미지</label>
|
||||
<input
|
||||
type="file"
|
||||
accept=".png, .jpg, .jpeg"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className={styles.button}
|
||||
>
|
||||
<EditIcon />
|
||||
<div>제출</div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
74
frontend/src/components/QuizForm/QuizsetForm.module.css
Normal file
74
frontend/src/components/QuizForm/QuizsetForm.module.css
Normal file
@ -0,0 +1,74 @@
|
||||
.quizsetForm {
|
||||
background: var(--background-color);
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.goBack {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
font-weight: 400;
|
||||
color: var(--text-color-secondary);
|
||||
stroke: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32px;
|
||||
line-height: 1.2;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.removeButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--accent-color);
|
||||
background-color: var(--accent-color);
|
||||
color: var(--on-primary);
|
||||
stroke: var(--on-primary);
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
font-weight: 700;
|
||||
align-self: end;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--primary-color);
|
||||
background-color: var(--primary-color);
|
||||
color: var(--on-primary);
|
||||
stroke: var(--on-primary);
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
font-weight: 700;
|
||||
align-self: end;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
2
frontend/src/components/QuizForm/index.js
Normal file
2
frontend/src/components/QuizForm/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as QuizCard } from './QuizCard';
|
||||
export { default as QuizsetForm } from './QuizsetForm';
|
22
frontend/src/components/QuizsetDetail/QuizsetDetail.jsx
Normal file
22
frontend/src/components/QuizsetDetail/QuizsetDetail.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import BackIcon from '/src/assets/icons/back.svg?react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styles from './QuizsetDetail.module.css';
|
||||
|
||||
export default function QuizsetDetail({ topic, title }) {
|
||||
return (
|
||||
<div className={styles.quizsetDetail}>
|
||||
<header className={styles.header}>
|
||||
<Link
|
||||
to={'..'}
|
||||
className={styles.goBack}
|
||||
>
|
||||
<BackIcon />
|
||||
<span>{topic}</span>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className={styles.title}>{title}</h1>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
.quizsetDetail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
background-color: var(--background-default);
|
||||
color: var(--text-color);
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.goBack {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
font-weight: 400;
|
||||
color: var(--text-color-secondary);
|
||||
stroke: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32px;
|
||||
line-height: 1.2;
|
||||
font-weight: 800;
|
||||
margin: 0;
|
||||
}
|
1
frontend/src/components/QuizsetDetail/index.js
Normal file
1
frontend/src/components/QuizsetDetail/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default as QuizsetDetail } from './QuizsetDetail';
|
@ -1,11 +1,12 @@
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import styles from './SideLink.module.css';
|
||||
|
||||
export default function SideLink({ children, to, end = false }) {
|
||||
export default function SideLink({ children, to, state = null, end = false }) {
|
||||
return (
|
||||
<li className={styles.list}>
|
||||
<NavLink
|
||||
to={to}
|
||||
state={{ from: state }}
|
||||
className={({ isActive }) => (isActive ? styles.active : styles.link)}
|
||||
end={end}
|
||||
>
|
||||
|
@ -44,5 +44,16 @@ export function useAuth() {
|
||||
});
|
||||
};
|
||||
|
||||
return { login, userRegister };
|
||||
const logout = () => {
|
||||
return instance
|
||||
.post(`${API_URL}/user/logout`)
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
setUserType(null);
|
||||
setToken(null);
|
||||
})
|
||||
.catch((e) => console.log(e));
|
||||
};
|
||||
|
||||
return { login, logout, userRegister };
|
||||
}
|
||||
|
10
frontend/src/hooks/api/useLectureDelete.js
Normal file
10
frontend/src/hooks/api/useLectureDelete.js
Normal file
@ -0,0 +1,10 @@
|
||||
import instance from '../../utils/axios/instance';
|
||||
import { API_URL } from '../../constants';
|
||||
|
||||
export function useLectureDelete() {
|
||||
const lectureDelete = (lectureId) => {
|
||||
return instance.delete(`${API_URL}/lecture/${lectureId}`);
|
||||
};
|
||||
|
||||
return { lectureDelete };
|
||||
}
|
10
frontend/src/hooks/api/useLectureEdit.js
Normal file
10
frontend/src/hooks/api/useLectureEdit.js
Normal file
@ -0,0 +1,10 @@
|
||||
import instance from '../../utils/axios/instance';
|
||||
import { API_URL } from '../../constants';
|
||||
|
||||
export function useLectureEdit() {
|
||||
const lectureEdit = (lectureId, lectureObject) => {
|
||||
return instance.put(`${API_URL}/lecture/${lectureId}`, lectureObject);
|
||||
};
|
||||
|
||||
return { lectureEdit };
|
||||
}
|
10
frontend/src/hooks/api/useQuizsetDetail.js
Normal file
10
frontend/src/hooks/api/useQuizsetDetail.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 useQuizsetDetail(id) {
|
||||
return useSuspenseQuery({
|
||||
queryKey: ['quizset', id],
|
||||
queryFn: () => instance.get(`${API_URL}/quiz/${id}`),
|
||||
});
|
||||
}
|
14
frontend/src/hooks/api/useQuizsetWrite.js
Normal file
14
frontend/src/hooks/api/useQuizsetWrite.js
Normal file
@ -0,0 +1,14 @@
|
||||
import instance from '../../utils/axios/instance';
|
||||
import { API_URL } from '../../constants';
|
||||
|
||||
export function useQuizsetWrite() {
|
||||
const quizsetWrite = (formData) => {
|
||||
return instance.post(`${API_URL}/quiz`, formData, {
|
||||
headers: {
|
||||
'Content-type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return { quizsetWrite };
|
||||
}
|
10
frontend/src/hooks/api/useQuizsets.js
Normal file
10
frontend/src/hooks/api/useQuizsets.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import instance from '../../utils/axios/instance';
|
||||
import { API_URL } from '../../constants';
|
||||
|
||||
export function useQuizsets() {
|
||||
return useQuery({
|
||||
queryKey: ['quizsetList'],
|
||||
queryFn: () => instance.get(`${API_URL}/quiz`),
|
||||
});
|
||||
}
|
@ -20,7 +20,7 @@ export default function useChatRoom(roomId) {
|
||||
}
|
||||
|
||||
chatClient.publish({
|
||||
destination: `/pub/message/${roomId}`,
|
||||
destination: `/pub/chat.message.${roomId}`,
|
||||
body: JSON.stringify({
|
||||
userId: USER_ID,
|
||||
name: userName,
|
||||
@ -32,7 +32,7 @@ export default function useChatRoom(roomId) {
|
||||
|
||||
useEffect(() => {
|
||||
client.onConnect = () => {
|
||||
client.subscribe(`/sub/channel/${roomId}`, (response) => {
|
||||
client.subscribe(`/exchange/chat.exchange/*.room.${roomId}`, (response) => {
|
||||
const data = JSON.parse(response.body);
|
||||
const { content: message, name } = data;
|
||||
|
||||
|
70
frontend/src/hooks/live/useRoom.js
Normal file
70
frontend/src/hooks/live/useRoom.js
Normal file
@ -0,0 +1,70 @@
|
||||
import { Room, RoomEvent } from 'livekit-client';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { API_URL, ROOM_URL } from '../../constants';
|
||||
import instance from '../../utils/axios/instance';
|
||||
|
||||
export default function useRoom(roomId) {
|
||||
const [room, setRoom] = useState(null);
|
||||
const [localTrack, setLocalTrack] = useState(null);
|
||||
const [remoteTracks, setRemoteTracks] = useState([]);
|
||||
const [mainTrack, setMainTrack] = useState(null);
|
||||
|
||||
const generateToken = useCallback(async () => {
|
||||
await instance.post(`${API_URL}/video/makeroom/${roomId}`);
|
||||
const { data } = await instance.post(`${API_URL}/video/joinroom/${roomId}`);
|
||||
|
||||
return data.token;
|
||||
}, [roomId]);
|
||||
|
||||
const leaveRoom = useCallback(async () => {
|
||||
console.log('leave room');
|
||||
await room?.disconnect();
|
||||
|
||||
setRoom(null);
|
||||
setLocalTrack(null);
|
||||
setRemoteTracks([]);
|
||||
}, [room]);
|
||||
|
||||
const joinRoom = useCallback(async () => {
|
||||
const token = await generateToken();
|
||||
const room = new Room();
|
||||
|
||||
room.prepareConnection(ROOM_URL, token);
|
||||
room.on(RoomEvent.TrackSubscribed, (_track, publication, participant) => {
|
||||
try {
|
||||
const identity = JSON.parse(participant.identity);
|
||||
const isTeacher = identity.role.startsWith('강사');
|
||||
if (isTeacher) {
|
||||
setMainTrack(publication.videoTrack);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('not json');
|
||||
} finally {
|
||||
setRemoteTracks((prev) => [
|
||||
...prev,
|
||||
{
|
||||
trackPublication: publication,
|
||||
participantIdentity: participant.identity,
|
||||
},
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
room.on(RoomEvent.TrackUnsubscribed, (_track, publication) => {
|
||||
console.log('unsubscribe remote');
|
||||
setRemoteTracks((prev) => prev.filter((track) => track.trackPublication !== publication));
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(token);
|
||||
await room.connect(ROOM_URL, token);
|
||||
await room.localParticipant.enableCameraAndMicrophone();
|
||||
setLocalTrack(room.localParticipant.videoTrackPublications.values().next().value.videoTrack);
|
||||
setRoom(room);
|
||||
} catch (error) {
|
||||
await leaveRoom();
|
||||
}
|
||||
}, [generateToken, leaveRoom]);
|
||||
|
||||
return { room, joinRoom, leaveRoom, localTrack, remoteTracks, mainTrack };
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
@ -10,9 +9,7 @@ const queryClient = new QueryClient({
|
||||
});
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
@ -1,36 +1,13 @@
|
||||
import styles from './LectureCreatePage.module.css';
|
||||
import { useRef } from 'react';
|
||||
import { LectureForm } from '../../components/LectureForm';
|
||||
import { useLectureCreate } from '../../hooks/api/useLectureCreate';
|
||||
|
||||
export default function LectureCreatePage() {
|
||||
// TODO: 디자인 필요
|
||||
const titleRef = useRef('');
|
||||
const descriptionRef = useRef('');
|
||||
const planRef = useRef('');
|
||||
const startDateRef = useRef('');
|
||||
const endDateRef = useRef('');
|
||||
const timeRef = useRef(null);
|
||||
const imageFileRef = useRef('');
|
||||
|
||||
const { lectureCreate } = useLectureCreate();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const lectureObject = {
|
||||
title: titleRef.current.value,
|
||||
description: descriptionRef.current.value,
|
||||
plan: planRef.current.value,
|
||||
startDate: new Date(startDateRef.current.value).toISOString(),
|
||||
endDate: new Date(endDateRef.current.value).toISOString(),
|
||||
time: timeRef.current.value,
|
||||
};
|
||||
|
||||
const handleSubmit = async (lectureObject, imageFile) => {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('lectureCreateRequest', new Blob([JSON.stringify(lectureObject)], { type: 'application/json' }));
|
||||
|
||||
const imageFile = imageFileRef.current.files[0] ?? null;
|
||||
if (imageFile) {
|
||||
formData.append('image', imageFile);
|
||||
}
|
||||
@ -40,72 +17,13 @@ export default function LectureCreatePage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
className={styles.createClass}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className={styles.inputField}>
|
||||
<label className={styles.label}>강의명</label>
|
||||
<input
|
||||
className={styles.input}
|
||||
ref={titleRef}
|
||||
type="text"
|
||||
placeholder="강의명을 입력하세요"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.inputField}>
|
||||
<label className={styles.label}>설명</label>
|
||||
<textarea
|
||||
ref={descriptionRef}
|
||||
className={styles.textarea}
|
||||
placeholder="강의에 대한 설명을 입력하세요"
|
||||
></textarea>
|
||||
</div>
|
||||
<div className={styles.inputField}>
|
||||
<label className={styles.label}>강의 계획</label>
|
||||
<textarea
|
||||
ref={planRef}
|
||||
className={styles.textarea}
|
||||
placeholder="강의 계획을 입력하세요"
|
||||
></textarea>
|
||||
</div>
|
||||
<div className={styles.inputField}>
|
||||
<label className={styles.label}>강의 기간</label>
|
||||
<input
|
||||
className={styles.input}
|
||||
ref={startDateRef}
|
||||
type="date"
|
||||
/>
|
||||
<input
|
||||
className={styles.input}
|
||||
ref={endDateRef}
|
||||
type="date"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.inputField}>
|
||||
<label className={styles.label}>수업 시간</label>
|
||||
<input
|
||||
type="text"
|
||||
ref={timeRef}
|
||||
className={styles.input}
|
||||
placeholder="실제 강의 진행 시간을 입력하세요"
|
||||
></input>
|
||||
</div>
|
||||
<div className={styles.inputField}>
|
||||
<label className={styles.label}>수업 이미지</label>
|
||||
<input
|
||||
type="file"
|
||||
ref={imageFileRef}
|
||||
accept=""
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className={styles.button}
|
||||
>
|
||||
<div>ㅁ</div>
|
||||
<div className={styles.buttonText}>강의 생성</div>
|
||||
</button>
|
||||
</form>
|
||||
<div>
|
||||
<h1>강의 생성</h1>
|
||||
<LectureForm
|
||||
title={'강의 생성'}
|
||||
onSubmit={handleSubmit}
|
||||
onCreate={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
34
frontend/src/pages/LectureEditPage/LectureEditPage.jsx
Normal file
34
frontend/src/pages/LectureEditPage/LectureEditPage.jsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { LectureForm } from '../../components/LectureForm';
|
||||
import { useLectureEdit } from '../../hooks/api/useLectureEdit';
|
||||
import { useParams, useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
export default function LecutreEditPage() {
|
||||
const { lectureId } = useParams();
|
||||
const location = useLocation();
|
||||
const initialData = location.state.from;
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { lectureEdit } = useLectureEdit();
|
||||
|
||||
const handleSubmit = async (lectureObject) => {
|
||||
const response = await lectureEdit(lectureId, lectureObject)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
navigate('..');
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
console.log(response?.data);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LectureForm
|
||||
initialValues={initialData}
|
||||
onSubmit={handleSubmit}
|
||||
title={'강의 홈'}
|
||||
topic={'강의 수정'}
|
||||
to={'..'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
1
frontend/src/pages/LectureEditPage/index.js
Normal file
1
frontend/src/pages/LectureEditPage/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './LectureEditPage';
|
@ -1,14 +1,25 @@
|
||||
import useBoundStore from '../../store';
|
||||
import ChatRoom from '../../components/ChatRoom/ChatRoom';
|
||||
import { LiveRoom } from '../../components/LiveRoom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import useRoom from '../../hooks/live/useRoom';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function LivePage() {
|
||||
const liveTabStatus = useBoundStore((state) => state.liveTabStatus);
|
||||
// const setLiveTabStatus = useBoundStore((state) => state.setLiveTabStatus);
|
||||
const { roomId } = useParams();
|
||||
const { room, joinRoom, localTrack, remoteTracks, mainTrack, leaveRoom } = useRoom(roomId);
|
||||
|
||||
useEffect(() => {
|
||||
if (!room) {
|
||||
joinRoom();
|
||||
}
|
||||
}, [joinRoom, room]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<main></main>
|
||||
<aside>{liveTabStatus === 'chat' ? <ChatRoom /> : <div>Quiz</div>}</aside>
|
||||
</>
|
||||
<LiveRoom
|
||||
room={room}
|
||||
localTrack={localTrack}
|
||||
remoteTracks={remoteTracks}
|
||||
leaveRoom={leaveRoom}
|
||||
mainTrack={mainTrack}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
11
frontend/src/pages/QuizsetDetailPage/QuizsetDetailPage.jsx
Normal file
11
frontend/src/pages/QuizsetDetailPage/QuizsetDetailPage.jsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { useQuizsetDetail } from '../../hooks/api/useQuizsetDetail';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { QuizsetDetail } from '../../components/QuizsetDetail';
|
||||
|
||||
export default function QuizsetListPage() {
|
||||
const { lectureId } = useParams();
|
||||
const { data } = useQuizsetDetail(lectureId);
|
||||
const quizset = data?.data ?? [];
|
||||
console.log(quizset);
|
||||
return <QuizsetDetail title={quizset.title} />;
|
||||
}
|
1
frontend/src/pages/QuizsetDetailPage/index.js
Normal file
1
frontend/src/pages/QuizsetDetailPage/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './QuizsetDetailPage';
|
27
frontend/src/pages/QuizsetListPage/QuizsetListPage.jsx
Normal file
27
frontend/src/pages/QuizsetListPage/QuizsetListPage.jsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { ArticleLink } from '../../components/ArticleLink';
|
||||
import ArticleBoard from '../../components/ArticleBoard/ArticleBoard';
|
||||
import { useQuizsets } from '../../hooks/api/useQuizsets';
|
||||
import { useParams } from 'react-router-dom';
|
||||
// import useBoundStore from '../../store';
|
||||
|
||||
export default function QuizsetListPage() {
|
||||
const { lectureId } = useParams();
|
||||
const { data } = useQuizsets(lectureId);
|
||||
const quizsets = data?.data ?? [];
|
||||
// const userType = useBoundStore((state) => state.userType);
|
||||
console.log(quizsets);
|
||||
return (
|
||||
<ArticleBoard
|
||||
title="퀴즈 목록"
|
||||
canCreate={true}
|
||||
>
|
||||
{quizsets.map?.((quizset) => (
|
||||
<ArticleLink
|
||||
key={`${quizset.quizSetId}`}
|
||||
title={quizset.title}
|
||||
to={`${quizset.quizSetId}`}
|
||||
/>
|
||||
))}
|
||||
</ArticleBoard>
|
||||
);
|
||||
}
|
1
frontend/src/pages/QuizsetListPage/index.js
Normal file
1
frontend/src/pages/QuizsetListPage/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './QuizsetListPage';
|
32
frontend/src/pages/QuizsetWritePage/QuizsetWritePage.jsx
Normal file
32
frontend/src/pages/QuizsetWritePage/QuizsetWritePage.jsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { QuizsetForm } from '../../components/QuizForm';
|
||||
import { useQuizsetWrite } from '../../hooks/api/useQuizsetWrite';
|
||||
|
||||
export default function QuizsetWritePage() {
|
||||
// TODO: lecture에서 이미지 전송 성공 후 해당 방법으로 이미지 파일 입력
|
||||
const { quizsetWrite } = useQuizsetWrite();
|
||||
const handleSubmit = async (e, title, quizzes, imageFile = null) => {
|
||||
e.preventDefault();
|
||||
const quizsetObject = {
|
||||
title,
|
||||
quizzes,
|
||||
};
|
||||
console.log(quizsetObject);
|
||||
console.log(imageFile);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('quizSetCreateRequest', new Blob([JSON.stringify(quizsetObject)], { type: 'application/json' }));
|
||||
if (imageFile) {
|
||||
formData.append('image', imageFile);
|
||||
}
|
||||
const response = await quizsetWrite(formData);
|
||||
console.log(response);
|
||||
};
|
||||
return (
|
||||
<QuizsetForm
|
||||
onSubmit={handleSubmit}
|
||||
headerTitle={'퀴즈 목록'}
|
||||
topic={'퀴즈 작성'}
|
||||
to={'..'}
|
||||
/>
|
||||
);
|
||||
}
|
1
frontend/src/pages/QuizsetWritePage/index.js
Normal file
1
frontend/src/pages/QuizsetWritePage/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './QuizsetWritePage';
|
@ -2,14 +2,17 @@ import { create } from 'zustand';
|
||||
import { userTypeSlice } from './userTypeSlice';
|
||||
import { tokenSlice } from './tokenSlice';
|
||||
import { userNameSlice } from './userNameSlice';
|
||||
import { liveSlice } from './liveSlice';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
const useBoundStore = create((...a) => ({
|
||||
// TODO: persist 옵션 추가
|
||||
...userTypeSlice(...a),
|
||||
...tokenSlice(...a),
|
||||
...userNameSlice(...a),
|
||||
...liveSlice(...a),
|
||||
}));
|
||||
const useBoundStore = create(
|
||||
persist(
|
||||
(...a) => ({
|
||||
...userTypeSlice(...a),
|
||||
...tokenSlice(...a),
|
||||
...userNameSlice(...a),
|
||||
}),
|
||||
{ name: 'bound-store' }
|
||||
)
|
||||
);
|
||||
|
||||
export default useBoundStore;
|
||||
|
@ -1,6 +0,0 @@
|
||||
// liveTabStatus: 'chat' | 'quiz'
|
||||
|
||||
export const liveSlice = (set) => ({
|
||||
liveTabStatus: 'chat',
|
||||
setLiveTabStatus: (liveTabStatus) => set({ liveTabStatus }),
|
||||
});
|
@ -6,7 +6,6 @@ const instance = axios.create({
|
||||
timeout: 1000,
|
||||
headers: {
|
||||
'Content-type': 'application/json;charset=utf-8',
|
||||
'Access-Control-Allow-Origin': import.meta.env.VITE_ORIGIN,
|
||||
},
|
||||
withCredentials: true,
|
||||
});
|
||||
@ -28,16 +27,25 @@ instance.interceptors.response.use(
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// TODO: api url update
|
||||
const REFRESH_API_URL = '/refresh-token';
|
||||
const REFRESH_API_URL = '/user/refresh';
|
||||
|
||||
return instance.post(REFRESH_API_URL).then((response) => {
|
||||
const { accessToken } = response.data;
|
||||
return instance
|
||||
.post(REFRESH_API_URL)
|
||||
.then((response) => {
|
||||
const { accessToken } = response.data;
|
||||
|
||||
useBoundStore.setState({ token: accessToken });
|
||||
error.config.headers.Authorization = `${accessToken}`;
|
||||
return instance(error.config);
|
||||
});
|
||||
console.log(accessToken);
|
||||
useBoundStore.setState({ token: accessToken });
|
||||
error.config.headers.Authorization = `${accessToken}`;
|
||||
return instance(error.config);
|
||||
})
|
||||
.catch((error) => {
|
||||
useBoundStore.setState({ token: null });
|
||||
console.log(error);
|
||||
console.log('---로그아웃----');
|
||||
// TODO: redirect to home
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { Client } from '@stomp/stompjs';
|
||||
import useBoundStore from '../../store';
|
||||
|
||||
export const chatClient = new Client({
|
||||
brokerURL: import.meta.env.VITE_CHAT_URL,
|
||||
// TODO: debug 제거
|
||||
debug: (str) => console.log(str),
|
||||
reconnectDelay: 5000,
|
||||
debug: import.meta.env.DEV ? (str) => console.log(str) : () => {},
|
||||
reconnectDelay: 3000,
|
||||
connectHeaders: {
|
||||
Authorization: useBoundStore.getState().token,
|
||||
},
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user