feat: 라이브 수업 구현
This commit is contained in:
parent
e3667f1b73
commit
ba6945de05
107
frontend/package-lock.json
generated
107
frontend/package-lock.json
generated
@ -8,6 +8,8 @@
|
|||||||
"name": "edufocus",
|
"name": "edufocus",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@livekit/components-react": "^2.4.3",
|
||||||
|
"@livekit/components-styles": "^1.0.12",
|
||||||
"@stomp/stompjs": "^7.0.0",
|
"@stomp/stompjs": "^7.0.0",
|
||||||
"@tanstack/react-query": "^5.49.2",
|
"@tanstack/react-query": "^5.49.2",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
@ -905,6 +907,31 @@
|
|||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.6.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.5.tgz",
|
||||||
|
"integrity": "sha512-8GrTWmoFhm5BsMZOTHeGD2/0FLKLQQHvO/ZmQga4tKempYRLz8aqJGqXVuQgisnMObq2YZ2SgkwctN1LOOxcqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.6.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.8.tgz",
|
||||||
|
"integrity": "sha512-kx62rP19VZ767Q653wsP1XZCGIirkE09E0QUGNYTM/ttbbQHqcGPdSfWFxUyyNLc/W6aoJRBajOSXhP6GXjC0Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.6.0",
|
||||||
|
"@floating-ui/utils": "^0.2.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.5.tgz",
|
||||||
|
"integrity": "sha512-sTcG+QZ6fdEUObICavU+aB3Mp8HY4n14wYHdxK4fXjPmv3PXZZeY5RaguJmGyeH/CJQhX3fqKUtS4qc1LoHwhQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.11.14",
|
"version": "0.11.14",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
||||||
@ -1009,6 +1036,55 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@livekit/components-core": {
|
||||||
|
"version": "0.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.11.2.tgz",
|
||||||
|
"integrity": "sha512-rXQ1OvyGe9gY8BCpH5FTr4Il17/sS/ecJQbG3PoOXAkQVl5JP965eqUPyKXZTdxNKlVLef00AygrO2pPArwOTA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "1.6.8",
|
||||||
|
"loglevel": "1.9.1",
|
||||||
|
"rxjs": "7.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@livekit/protocol": "^1.16.0",
|
||||||
|
"livekit-client": "^2.4.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@livekit/components-react": {
|
||||||
|
"version": "2.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@livekit/components-react/-/components-react-2.4.3.tgz",
|
||||||
|
"integrity": "sha512-XhCvwFvNjhBJcoQHIY4Hk6MBp7mM9q0n0i7sN/xK3fB1DSjkxIkpc7lh/+Pjqdu6F6OJT3MjwNFYnftqy6kcmw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@livekit/components-core": "0.11.2",
|
||||||
|
"clsx": "2.1.1",
|
||||||
|
"usehooks-ts": "3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@livekit/protocol": "^1.16.0",
|
||||||
|
"livekit-client": "^2.4.0",
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@livekit/components-styles": {
|
||||||
|
"version": "1.0.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@livekit/components-styles/-/components-styles-1.0.12.tgz",
|
||||||
|
"integrity": "sha512-Hsxkfq240w0tMPtkQTHQotpkYfIY4lhP2pzegvOIIV/nYxj8LeRYypUjxJpFw3s6jQcV/WQS7oCYmFQdy98Jtw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@livekit/protocol": {
|
"node_modules/@livekit/protocol": {
|
||||||
"version": "1.19.1",
|
"version": "1.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.19.1.tgz",
|
||||||
@ -2420,6 +2496,15 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/clsx": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@ -4324,6 +4409,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.debounce": {
|
||||||
|
"version": "4.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||||
|
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
@ -5244,7 +5335,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
@ -5873,6 +5963,21 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/usehooks-ts": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-bBIa7yUyPhE1BCc0GmR96VU/15l/9gP1Ch5mYdLcFBaFGQsdmXkvjV0TtOqW1yUd6VjIwDunm+flSciCQXujiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash.debounce": "^4.0.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.15.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17 || ^18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.3.3",
|
"version": "5.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz",
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
"test:run": "vitest run"
|
"test:run": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@livekit/components-react": "^2.4.3",
|
||||||
|
"@livekit/components-styles": "^1.0.12",
|
||||||
"@stomp/stompjs": "^7.0.0",
|
"@stomp/stompjs": "^7.0.0",
|
||||||
"@tanstack/react-query": "^5.49.2",
|
"@tanstack/react-query": "^5.49.2",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
|
@ -1,42 +1,112 @@
|
|||||||
import styles from './ChatRoom.module.css';
|
import styles from './ChatRoom.module.css';
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import SendIcon from '/src/assets/icons/send.svg?react';
|
import SendIcon from '/src/assets/icons/send.svg?react';
|
||||||
import useChatRoom from '../../hooks/chat/useChatRoom';
|
import { cloneElement, useEffect, useRef } from 'react';
|
||||||
|
import { ChatEntry, useChat, useMaybeLayoutContext } from '@livekit/components-react';
|
||||||
|
|
||||||
export default function ChatRoom() {
|
export default function ChatRoom({ ...props }) {
|
||||||
const { roomId } = useParams();
|
const chatInputRef = useRef(null);
|
||||||
const { messages, handleSubmit, inputRef, chatListRef } = useChatRoom(roomId);
|
const ulRef = useRef(null);
|
||||||
|
|
||||||
|
const { send, chatMessages, isSending } = useChat();
|
||||||
|
|
||||||
|
const layoutContext = useMaybeLayoutContext();
|
||||||
|
const lastReadMsgAt = useRef(0);
|
||||||
|
|
||||||
|
async function handleChatSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (chatInputRef.current && chatInputRef.current.value.trim() !== '') {
|
||||||
|
if (send) {
|
||||||
|
await send(chatInputRef.current.value);
|
||||||
|
chatInputRef.current.value = '';
|
||||||
|
chatInputRef.current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ulRef) {
|
||||||
|
ulRef.current?.scrollTo({ top: ulRef.current.scrollHeight });
|
||||||
|
}
|
||||||
|
}, [ulRef, chatMessages]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!layoutContext || chatMessages.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
layoutContext.widget.state?.showChat &&
|
||||||
|
chatMessages.length > 0 &&
|
||||||
|
lastReadMsgAt.current !== chatMessages[chatMessages.length - 1]?.timestamp
|
||||||
|
) {
|
||||||
|
lastReadMsgAt.current = chatMessages[chatMessages.length - 1]?.timestamp;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unreadMessageCount = chatMessages.filter(
|
||||||
|
(msg) => !lastReadMsgAt.current || msg.timestamp > lastReadMsgAt.current
|
||||||
|
).length;
|
||||||
|
|
||||||
|
const { widget } = layoutContext;
|
||||||
|
if (unreadMessageCount > 0 && widget.state?.unreadMessages !== unreadMessageCount) {
|
||||||
|
widget.dispatch?.({ msg: 'unread_msg', count: unreadMessageCount });
|
||||||
|
}
|
||||||
|
}, [chatMessages, layoutContext, layoutContext.widget]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.room}>
|
<div
|
||||||
|
{...props}
|
||||||
|
className="lk-chat"
|
||||||
|
>
|
||||||
<h2 className={styles.title}>채팅</h2>
|
<h2 className={styles.title}>채팅</h2>
|
||||||
<ol
|
|
||||||
className={styles.messageList}
|
<ul
|
||||||
ref={chatListRef}
|
className="lk-list lk-chat-messages"
|
||||||
|
ref={ulRef}
|
||||||
>
|
>
|
||||||
{messages.map?.((message) => (
|
{props.children
|
||||||
<li
|
? chatMessages.map((msg, idx) =>
|
||||||
key={message.id}
|
cloneElement(props.children, {
|
||||||
className={message.isMine ? styles.my : styles.your}
|
entry: msg,
|
||||||
>
|
key: msg.id ?? idx,
|
||||||
<span className={styles.name}>{message.name}</span>
|
})
|
||||||
<span className={styles.bubble}>{message.text}</span>
|
)
|
||||||
</li>
|
: chatMessages.map((msg, idx, allMsg) => {
|
||||||
))}
|
const hideName = idx >= 1 && allMsg[idx - 1].from === msg.from;
|
||||||
</ol>
|
const hideTimestamp = idx >= 1 && msg.timestamp - allMsg[idx - 1].timestamp < 60_000;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChatEntry
|
||||||
|
key={msg.id ?? idx}
|
||||||
|
hideName={hideName}
|
||||||
|
hideTimestamp={hideName === false ? false : hideTimestamp}
|
||||||
|
entry={msg}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
<form
|
<form
|
||||||
action="POST"
|
className="lk-chat-form"
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleChatSubmit}
|
||||||
className={styles.form}
|
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
className="lk-form-control lk-chat-form-input"
|
||||||
|
disabled={isSending}
|
||||||
|
ref={chatInputRef}
|
||||||
type="text"
|
type="text"
|
||||||
ref={inputRef}
|
placeholder="메시지"
|
||||||
|
onInput={(ev) => ev.stopPropagation()}
|
||||||
|
onKeyDown={(ev) => ev.stopPropagation()}
|
||||||
|
onKeyUp={(ev) => ev.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
<button type="submit">
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={`lk-button lk-chat-form-button ${styles.button}`}
|
||||||
|
disabled={isSending}
|
||||||
|
>
|
||||||
<SendIcon />
|
<SendIcon />
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 400px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
@ -20,8 +20,9 @@
|
|||||||
align-items: start;
|
align-items: start;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
max-height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 16px;
|
padding: 0 8px 8px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@ -91,8 +92,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
background-color: var(--background);
|
background-color: black;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color-tertiary);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -103,7 +104,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
color: var(--text-color);
|
/* color: var(--text-color); */
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@ -124,7 +125,11 @@
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: var(--background);
|
background-color: black;
|
||||||
stroke: var(--text-color);
|
stroke: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
stroke: white;
|
||||||
|
}
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
import styles from './InfoEditForm.module.css';
|
import styles from './InfoEditForm.module.css';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
export default function InfoEditForm({ onSubmit }) {
|
export default function InfoEditForm() {
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [useremail, setUseremail] = useState('');
|
const [useremail, setUseremail] = useState('');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form className={styles.infoEditForm}>
|
||||||
onSubmit={(e) => onSubmit(e, username, useremail)}
|
|
||||||
className={styles.infoEditForm}
|
|
||||||
>
|
|
||||||
<p className={styles.textHeading}>이름 변경</p>
|
<p className={styles.textHeading}>이름 변경</p>
|
||||||
<div className={styles.inputBox}>
|
<div className={styles.inputBox}>
|
||||||
<label
|
<label
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
export { default as LiveAudio } from './LiveAudio';
|
|
@ -1,46 +1,99 @@
|
|||||||
import styles from './LiveRoom.module.css';
|
import { isEqualTrackRef, isTrackReference } from '@livekit/components-core';
|
||||||
|
import {
|
||||||
|
CarouselLayout,
|
||||||
|
ConnectionStateToast,
|
||||||
|
ControlBar,
|
||||||
|
FocusLayout,
|
||||||
|
FocusLayoutContainer,
|
||||||
|
GridLayout,
|
||||||
|
LayoutContextProvider,
|
||||||
|
ParticipantTile,
|
||||||
|
RoomAudioRenderer,
|
||||||
|
useCreateLayoutContext,
|
||||||
|
usePinnedTracks,
|
||||||
|
useTracks,
|
||||||
|
} from '@livekit/components-react';
|
||||||
|
import { RoomEvent, Track } from 'livekit-client';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
import ChatRoom from '../ChatRoom/ChatRoom';
|
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 }) {
|
export default function LiveRoom() {
|
||||||
|
const lastAutoFocusedScreenShareTrack = useRef(null);
|
||||||
|
|
||||||
|
const tracks = useTracks(
|
||||||
|
[
|
||||||
|
{ source: Track.Source.Camera, withPlaceholder: true },
|
||||||
|
{ source: Track.Source.ScreenShare, withPlaceholder: false },
|
||||||
|
],
|
||||||
|
{ updateOnlyOn: [RoomEvent.ActiveSpeakersChanged], onlySubscribed: false }
|
||||||
|
);
|
||||||
|
const layoutContext = useCreateLayoutContext();
|
||||||
|
|
||||||
|
const screenShareTracks = tracks
|
||||||
|
.filter(isTrackReference)
|
||||||
|
.filter((track) => track.publication.source === Track.Source.ScreenShare);
|
||||||
|
|
||||||
|
const focusTrack = usePinnedTracks(layoutContext)?.[0];
|
||||||
|
const carouselTracks = tracks.filter((track) => !isEqualTrackRef(track, focusTrack));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
screenShareTracks.some((track) => track.publication.isSubscribed) &&
|
||||||
|
lastAutoFocusedScreenShareTrack.current === null
|
||||||
|
) {
|
||||||
|
layoutContext.pin.dispatch?.({ msg: 'set_pin', trackReference: screenShareTracks[0] });
|
||||||
|
lastAutoFocusedScreenShareTrack.current = screenShareTracks[0];
|
||||||
|
} else if (
|
||||||
|
lastAutoFocusedScreenShareTrack.current &&
|
||||||
|
!screenShareTracks.some(
|
||||||
|
(track) => track.publication.trackSid === lastAutoFocusedScreenShareTrack.current?.publication?.trackSid
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
layoutContext.pin.dispatch?.({ msg: 'clear_pin' });
|
||||||
|
lastAutoFocusedScreenShareTrack.current = null;
|
||||||
|
}
|
||||||
|
if (focusTrack && !isTrackReference(focusTrack)) {
|
||||||
|
const updatedFocusTrack = tracks.find(
|
||||||
|
(tr) => tr.participant.identity === focusTrack.participant.identity && tr.source === focusTrack.source
|
||||||
|
);
|
||||||
|
if (updatedFocusTrack !== focusTrack && isTrackReference(updatedFocusTrack)) {
|
||||||
|
layoutContext.pin.dispatch?.({ msg: 'set_pin', trackReference: updatedFocusTrack });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
screenShareTracks.map((ref) => `${ref.publication.trackSid}_${ref.publication.isSubscribed}`).join(),
|
||||||
|
focusTrack?.publication?.trackSid,
|
||||||
|
tracks,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="lk-video-conference">
|
||||||
<main>
|
<LayoutContextProvider value={layoutContext}>
|
||||||
{room ? (
|
<div className="lk-video-conference-inner">
|
||||||
<>
|
{!focusTrack ? (
|
||||||
<div className={styles.videoWrapper}>
|
<div className="lk-grid-layout-wrapper">
|
||||||
{localTrack && (
|
<GridLayout tracks={tracks}>
|
||||||
<LiveVideo
|
<ParticipantTile />
|
||||||
track={localTrack}
|
</GridLayout>
|
||||||
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>
|
</div>
|
||||||
{mainTrack && <LiveVideo track={mainTrack} />}
|
) : (
|
||||||
</>
|
<div className="lk-focus-layout-wrapper">
|
||||||
) : (
|
<FocusLayoutContainer>
|
||||||
<LoadingIndicator fill />
|
<CarouselLayout tracks={carouselTracks}>
|
||||||
)}
|
<ParticipantTile />
|
||||||
</main>
|
</CarouselLayout>
|
||||||
<aside>
|
{focusTrack && <FocusLayout trackRef={focusTrack} />}
|
||||||
|
</FocusLayoutContainer>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ControlBar controls={{ chat: false, leave: false }} />
|
||||||
|
</div>
|
||||||
<ChatRoom />
|
<ChatRoom />
|
||||||
<button onClick={leaveRoom}>나가기</button>
|
</LayoutContextProvider>
|
||||||
</aside>
|
<RoomAudioRenderer />
|
||||||
</>
|
<ConnectionStateToast />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
|
.main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.videoWrapper {
|
.videoWrapper {
|
||||||
flex-grow: 0;
|
flex: 0 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
& > audio {
|
& > audio {
|
||||||
display: none;
|
display: none;
|
||||||
@ -21,7 +27,41 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.myVideo {
|
.mainContent {
|
||||||
border-radius: 12px;
|
display: flex;
|
||||||
border: 2px solid var(--border-color);
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
& > video {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 208px);
|
||||||
|
object-fit: contain;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.controlBar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 80px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
& > button {
|
||||||
|
background-color: var(--background-color-secondary);
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--background-color-tertiary);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
.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 +0,0 @@
|
|||||||
export { default as LiveVideo } from './LiveVideo';
|
|
@ -1,9 +1,10 @@
|
|||||||
import styles from './LoadingIndicator.module.css';
|
import styles from './LoadingIndicator.module.css';
|
||||||
|
|
||||||
export default function LoadingIndicator({ fill = false }) {
|
export default function LoadingIndicator({ fill = false, label }) {
|
||||||
return fill ? (
|
return fill ? (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<div className={styles.indicator} />
|
<div className={styles.indicator} />
|
||||||
|
{label && <div className={styles.label}>{label}</div>}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.indicator} />
|
<div className={styles.indicator} />
|
||||||
|
@ -4,10 +4,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes wave {
|
||||||
|
0% {
|
||||||
|
rotate: -5deg;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
rotate: 5deg;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
rotate: -5deg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@ -22,3 +38,11 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
animation: spin 2s cubic-bezier(0.645, 0.045, 0.355, 1) infinite;
|
animation: spin 2s cubic-bezier(0.645, 0.045, 0.355, 1) infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-color);
|
||||||
|
animation: wave 1.5s ease infinite;
|
||||||
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { useState, useRef } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
import styles from './PasswordChangeForm.module.css';
|
import styles from './PasswordChangeForm.module.css';
|
||||||
|
|
||||||
export default function PasswordChangeForm({ onSubmit, onPwError = false }) {
|
export default function PasswordChangeForm() {
|
||||||
// TODO: onPwError(현재 비밀번호와 같음) 시 응답을 받아 표시
|
|
||||||
const [errorConfirmMessage, setErrorConfirmMessage] = useState(false);
|
const [errorConfirmMessage, setErrorConfirmMessage] = useState(false);
|
||||||
const [errorSameMessage, setErrorSameMessage] = useState(false);
|
const [errorSameMessage, setErrorSameMessage] = useState(false);
|
||||||
const currentPasswordRef = useRef('');
|
const currentPasswordRef = useRef('');
|
||||||
const newPasswordRef = useRef('');
|
const newPasswordRef = useRef('');
|
||||||
const confirmPasswordRef = useRef('');
|
const confirmPasswordRef = useRef('');
|
||||||
|
const userPassword = '1234';
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -15,15 +15,13 @@ export default function PasswordChangeForm({ onSubmit, onPwError = false }) {
|
|||||||
const newPassword = newPasswordRef.current.value;
|
const newPassword = newPasswordRef.current.value;
|
||||||
const confirmPassword = confirmPasswordRef.current.value;
|
const confirmPassword = confirmPasswordRef.current.value;
|
||||||
|
|
||||||
|
if (currentPassword === userPassword) {
|
||||||
|
setErrorSameMessage(false);
|
||||||
|
} else {
|
||||||
|
setErrorSameMessage(true);
|
||||||
|
}
|
||||||
if (newPassword === confirmPassword) {
|
if (newPassword === confirmPassword) {
|
||||||
setErrorConfirmMessage(false);
|
setErrorConfirmMessage(false);
|
||||||
onSubmit(currentPassword, newPassword, confirmPassword);
|
|
||||||
|
|
||||||
if (onPwError) {
|
|
||||||
setErrorSameMessage(true);
|
|
||||||
} else {
|
|
||||||
setErrorSameMessage(false);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setErrorConfirmMessage(true);
|
setErrorConfirmMessage(true);
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import styles from './QuizCard.module.css';
|
import styles from './QuizCard.module.css';
|
||||||
|
|
||||||
export default function QuizCard({ quiz, updateQuiz, deleteQuiz }) {
|
export default function QuizCard({ quiz, index, updateQuiz }) {
|
||||||
const [question, setQuestion] = useState(quiz.question || '');
|
const [question, setQuestion] = useState(quiz.question || '');
|
||||||
const [answer, setAnswer] = useState(quiz.answer || '');
|
const [answer, setAnswer] = useState(quiz.answer || '');
|
||||||
const [choices, setChoices] = useState(quiz.choices || []);
|
const [choices, setChoices] = useState(quiz.choices || []);
|
||||||
const [imageFile, setImageFile] = useState(quiz.imageFile || null);
|
|
||||||
|
|
||||||
const handleChoiceChange = (num, content) => {
|
const handleChoiceChange = (num, content) => {
|
||||||
const updatedChoices = choices.map((choice) =>
|
const updatedChoices = choices.map((choice) => (choice.num === num ? { ...choice, content } : choice));
|
||||||
choice.num === num ? { ...choice, content } : choice
|
|
||||||
);
|
|
||||||
setChoices(updatedChoices);
|
setChoices(updatedChoices);
|
||||||
updateQuiz(quiz.id, { ...quiz, question, answer, choices: updatedChoices, imageFile });
|
updateQuiz(index, { question, answer, choices: updatedChoices });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddChoice = () => {
|
const handleAddChoice = () => {
|
||||||
@ -20,7 +17,7 @@ export default function QuizCard({ quiz, updateQuiz, deleteQuiz }) {
|
|||||||
const newChoice = { num: choices.length + 1, content: '' };
|
const newChoice = { num: choices.length + 1, content: '' };
|
||||||
const updatedChoices = [...choices, newChoice];
|
const updatedChoices = [...choices, newChoice];
|
||||||
setChoices(updatedChoices);
|
setChoices(updatedChoices);
|
||||||
updateQuiz(quiz.id, { ...quiz, question, answer, choices: updatedChoices, imageFile });
|
updateQuiz(index, { question, answer, choices: updatedChoices });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -28,29 +25,19 @@ export default function QuizCard({ quiz, updateQuiz, deleteQuiz }) {
|
|||||||
if (choices.length > 0) {
|
if (choices.length > 0) {
|
||||||
const updatedChoices = choices.slice(0, -1);
|
const updatedChoices = choices.slice(0, -1);
|
||||||
setChoices(updatedChoices);
|
setChoices(updatedChoices);
|
||||||
updateQuiz(quiz.id, { ...quiz, question, answer, choices: updatedChoices, imageFile });
|
updateQuiz(index, { question, answer, choices: updatedChoices });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileChange = (e) => {
|
|
||||||
const file = e.target.files[0] ?? null;
|
|
||||||
setImageFile(file);
|
|
||||||
updateQuiz(quiz.id, { ...quiz, question, answer, choices, imageFile: file });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.card}>
|
<div className={styles.card}>
|
||||||
<div className={styles.header}>
|
|
||||||
<span>퀴즈 생성 카드</span>
|
|
||||||
<span onClick={() => deleteQuiz(quiz.id)}>X</span> {/* id를 기반으로 삭제 */}
|
|
||||||
</div>
|
|
||||||
<label>질문</label>
|
<label>질문</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={question}
|
value={question}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setQuestion(e.target.value);
|
setQuestion(e.target.value);
|
||||||
updateQuiz(quiz.id, { ...quiz, question: e.target.value, answer, choices, imageFile });
|
updateQuiz(index, { question: e.target.value, answer, choices });
|
||||||
}}
|
}}
|
||||||
placeholder="질문 내용을 입력하세요"
|
placeholder="질문 내용을 입력하세요"
|
||||||
/>
|
/>
|
||||||
@ -60,7 +47,7 @@ export default function QuizCard({ quiz, updateQuiz, deleteQuiz }) {
|
|||||||
value={answer}
|
value={answer}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setAnswer(e.target.value);
|
setAnswer(e.target.value);
|
||||||
updateQuiz(quiz.id, { ...quiz, question, answer: e.target.value, choices, imageFile });
|
updateQuiz(index, { question, answer: e.target.value, choices });
|
||||||
}}
|
}}
|
||||||
placeholder="정답을 입력하세요"
|
placeholder="정답을 입력하세요"
|
||||||
/>
|
/>
|
||||||
@ -68,10 +55,18 @@ export default function QuizCard({ quiz, updateQuiz, deleteQuiz }) {
|
|||||||
<span>Tip: 선택지를 넣지 않는다면 단답형 문제가 됩니다</span>
|
<span>Tip: 선택지를 넣지 않는다면 단답형 문제가 됩니다</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.buttonsWrapper}>
|
<div className={styles.buttonsWrapper}>
|
||||||
<button type="button" onClick={handleAddChoice} className={styles.button}>
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAddChoice}
|
||||||
|
className={styles.button}
|
||||||
|
>
|
||||||
선택지 추가하기
|
선택지 추가하기
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={handlePopChoice} className={styles.removeButton}>
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handlePopChoice}
|
||||||
|
className={styles.removeButton}
|
||||||
|
>
|
||||||
선택지 줄이기
|
선택지 줄이기
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -86,8 +81,6 @@ export default function QuizCard({ quiz, updateQuiz, deleteQuiz }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<label>퀴즈 이미지</label>
|
|
||||||
<input type="file" accept=".png, .jpg, .jpeg" onChange={handleFileChange} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -8,12 +8,6 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonsWrapper {
|
.buttonsWrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -6,33 +6,32 @@ import BackIcon from '/src/assets/icons/back.svg?react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
export default function QuizsetForm({ headerTitle, topic, to, onSubmit }) {
|
export default function QuizsetForm({ headerTitle, topic, to, onSubmit }) {
|
||||||
|
// TODO: 디자인 만들기 및 스타일 적용
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
const [quizzes, setQuizzes] = useState([]);
|
const [quizzes, setQuizzes] = useState([]);
|
||||||
const [quizId, setQuizId] = useState(0);
|
const [imageFile, setImageFile] = useState(null);
|
||||||
|
|
||||||
const handleAddQuiz = () => {
|
const handleAddQuiz = () => {
|
||||||
setQuizzes([
|
setQuizzes([...quizzes, { question: '', answer: '', choices: [] }]);
|
||||||
...quizzes,
|
|
||||||
{ id: quizId, question: '', answer: '', choices: [], imageFile: null },
|
|
||||||
]);
|
|
||||||
setQuizId(quizId + 1);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateQuiz = (id, updatedQuiz) => {
|
const updateQuiz = (index, updatedQuiz) => {
|
||||||
const updatedQuizzes = quizzes.map((quiz) =>
|
const updatedQuizzes = quizzes.map((quiz, i) => (i === index ? updatedQuiz : quiz));
|
||||||
quiz.id === id ? updatedQuiz : quiz
|
|
||||||
);
|
|
||||||
setQuizzes(updatedQuizzes);
|
setQuizzes(updatedQuizzes);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteQuiz = (id) => {
|
const handleFileChange = (e) => {
|
||||||
setQuizzes(quizzes.filter((quiz) => quiz.id !== id));
|
const file = e.target.files?.[0];
|
||||||
|
setImageFile(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.quizsetForm}>
|
<div className={styles.quizsetForm}>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<Link to={to} className={styles.goBack}>
|
<Link
|
||||||
|
to={to}
|
||||||
|
className={styles.goBack}
|
||||||
|
>
|
||||||
<BackIcon />
|
<BackIcon />
|
||||||
<span>{headerTitle}</span>
|
<span>{headerTitle}</span>
|
||||||
</Link>
|
</Link>
|
||||||
@ -40,7 +39,7 @@ export default function QuizsetForm({ headerTitle, topic, to, onSubmit }) {
|
|||||||
</header>
|
</header>
|
||||||
<form
|
<form
|
||||||
className={styles.form}
|
className={styles.form}
|
||||||
onSubmit={(e) => onSubmit(e, title, quizzes)}
|
onSubmit={(e) => onSubmit(e, title, quizzes, imageFile)}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -48,12 +47,12 @@ export default function QuizsetForm({ headerTitle, topic, to, onSubmit }) {
|
|||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
placeholder="퀴즈셋 제목을 입력해주세요"
|
placeholder="퀴즈셋 제목을 입력해주세요"
|
||||||
/>
|
/>
|
||||||
{quizzes.map((quiz) => (
|
{quizzes.map((quiz, index) => (
|
||||||
<QuizCard
|
<QuizCard
|
||||||
key={quiz.id}
|
key={index}
|
||||||
quiz={quiz}
|
quiz={quiz}
|
||||||
|
index={index}
|
||||||
updateQuiz={updateQuiz}
|
updateQuiz={updateQuiz}
|
||||||
deleteQuiz={deleteQuiz}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<button
|
<button
|
||||||
@ -63,7 +62,16 @@ export default function QuizsetForm({ headerTitle, topic, to, onSubmit }) {
|
|||||||
>
|
>
|
||||||
퀴즈 추가하기
|
퀴즈 추가하기
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" className={styles.button}>
|
<label>퀴즈 이미지</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".png, .jpg, .jpeg"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={styles.button}
|
||||||
|
>
|
||||||
<EditIcon />
|
<EditIcon />
|
||||||
<div>제출</div>
|
<div>제출</div>
|
||||||
</button>
|
</button>
|
||||||
|
@ -2,7 +2,7 @@ import BackIcon from '/src/assets/icons/back.svg?react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import styles from './QuizsetDetail.module.css';
|
import styles from './QuizsetDetail.module.css';
|
||||||
|
|
||||||
export default function QuizsetDetail({ topic, title, quizzes = [], onDelete }) {
|
export default function QuizsetDetail({ topic, title }) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.quizsetDetail}>
|
<div className={styles.quizsetDetail}>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
@ -17,24 +17,6 @@ export default function QuizsetDetail({ topic, title, quizzes = [], onDelete })
|
|||||||
<h1 className={styles.title}>{title}</h1>
|
<h1 className={styles.title}>{title}</h1>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div>
|
|
||||||
{quizzes.map((quiz, index) => (
|
|
||||||
<div key={index}>
|
|
||||||
<div>질문 : {quiz.question}</div>
|
|
||||||
<img
|
|
||||||
src={quiz.image}
|
|
||||||
alt="강의 이미지"
|
|
||||||
/>
|
|
||||||
<div>정답이 응답에 없네요</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onDelete}
|
|
||||||
>
|
|
||||||
퀴즈셋 삭제
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -49,29 +49,15 @@ export function useAuth() {
|
|||||||
.post(`${API_URL}/user/logout`)
|
.post(`${API_URL}/user/logout`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
console.log(response);
|
console.log(response);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
setUserType(null);
|
setUserType(null);
|
||||||
setToken(null);
|
setToken(null);
|
||||||
})
|
});
|
||||||
.catch((e) => console.log(e));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateInfo = (name, email) => {
|
return { login, logout, userRegister };
|
||||||
const infoBody = {
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
};
|
|
||||||
return instance.put(`${API_URL}/user/updateinfo`, infoBody);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatePassword = (currentPw, newPw, newPwCheck) => {
|
|
||||||
const passwordBody = {
|
|
||||||
currentPassword: currentPw,
|
|
||||||
newPassword: newPw,
|
|
||||||
newPasswordCheck: newPwCheck,
|
|
||||||
};
|
|
||||||
console.log(passwordBody);
|
|
||||||
return instance.put(`${API_URL}/user/updatepassword`, passwordBody);
|
|
||||||
};
|
|
||||||
|
|
||||||
return { login, logout, userRegister, updateInfo, updatePassword };
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import instance from '../../utils/axios/instance';
|
|
||||||
import { API_URL } from '../../constants';
|
|
||||||
|
|
||||||
export function useQuizsetDelete() {
|
|
||||||
const quizsetDelete = (quizsetId) => {
|
|
||||||
return instance.delete(`${API_URL}/quiz/teacher/${quizsetId}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return { quizsetDelete };
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
import instance from '../../utils/axios/instance';
|
|
||||||
import { API_URL } from '../../constants';
|
|
||||||
|
|
||||||
export function useQuizsetEdit() {
|
|
||||||
const quizsetEdit = (quizsetId, quizsetObject) => {
|
|
||||||
return instance.put(`${API_URL}/lecture/${quizsetId}`, quizsetObject);
|
|
||||||
};
|
|
||||||
|
|
||||||
return { quizsetEdit };
|
|
||||||
}
|
|
@ -47,9 +47,7 @@ export default function useChatRoom(roomId) {
|
|||||||
}, [client, roomId]);
|
}, [client, roomId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chatListRef.current.scrollHeight - chatListRef.current.scrollTop - chatListRef.current.clientHeight < 200) {
|
chatListRef.current.scrollTop = chatListRef.current.scrollHeight;
|
||||||
chatListRef.current.scrollTop = chatListRef.current.scrollHeight;
|
|
||||||
}
|
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,70 +0,0 @@
|
|||||||
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 };
|
|
||||||
}
|
|
@ -17,12 +17,13 @@ export default function LectureCreatePage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LectureForm
|
<div>
|
||||||
title={'강의 홈'}
|
<h1>강의 생성</h1>
|
||||||
topic={'강의 생성'}
|
<LectureForm
|
||||||
to={'..'}
|
title={'강의 생성'}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onCreate={true}
|
onCreate={true}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -21,12 +21,14 @@ export default function LecutreEditPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LectureForm
|
<div>
|
||||||
initialValues={initialData}
|
<LectureForm
|
||||||
onSubmit={handleSubmit}
|
initialValues={initialData}
|
||||||
title={'강의 홈'}
|
onSubmit={handleSubmit}
|
||||||
topic={'강의 수정'}
|
title={'강의 홈'}
|
||||||
to={'..'}
|
topic={'강의 수정'}
|
||||||
/>
|
to={'..'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,41 @@
|
|||||||
import { LiveRoom } from '../../components/LiveRoom';
|
import { LiveRoom } from '../../components/LiveRoom';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import useRoom from '../../hooks/live/useRoom';
|
import { useCallback, useEffect } from 'react';
|
||||||
import { useEffect } from 'react';
|
import { LiveKitRoom } from '@livekit/components-react';
|
||||||
|
import instance from '../../utils/axios/instance';
|
||||||
|
import { API_URL, ROOM_URL } from '../../constants';
|
||||||
|
import useBoundStore from '../../store';
|
||||||
|
import '@livekit/components-styles';
|
||||||
|
|
||||||
export default function LivePage() {
|
export default function LivePage() {
|
||||||
const { roomId } = useParams();
|
const { roomId } = useParams();
|
||||||
const { room, joinRoom, localTrack, remoteTracks, mainTrack, leaveRoom } = useRoom(roomId);
|
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 liveToken = useBoundStore((state) => state.liveToken);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!room) {
|
if (!liveToken) {
|
||||||
joinRoom();
|
generateToken().then((token) => {
|
||||||
|
useBoundStore.setState({ liveToken: token });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [joinRoom, room]);
|
}, [generateToken, liveToken]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LiveRoom
|
liveToken && (
|
||||||
room={room}
|
<LiveKitRoom
|
||||||
localTrack={localTrack}
|
token={liveToken}
|
||||||
remoteTracks={remoteTracks}
|
serverUrl={ROOM_URL}
|
||||||
leaveRoom={leaveRoom}
|
connect={true}
|
||||||
mainTrack={mainTrack}
|
data-lk-theme="default"
|
||||||
/>
|
>
|
||||||
|
<LiveRoom />
|
||||||
|
</LiveKitRoom>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,5 @@
|
|||||||
import { InfoEditForm } from '../../components/InfoEditForm';
|
import { InfoEditForm } from '../../components/InfoEditForm';
|
||||||
import { useAuth } from '../../hooks/api/useAuth';
|
|
||||||
|
|
||||||
export default function MyInfoChangePage() {
|
export default function MyInfoChangePage() {
|
||||||
const { updateInfo } = useAuth();
|
return <InfoEditForm />;
|
||||||
|
|
||||||
const handleSubmit = async (e, username, useremail) => {
|
|
||||||
e.preventDefault();
|
|
||||||
await updateInfo(username, useremail)
|
|
||||||
.then((res) => console.log(res))
|
|
||||||
.catch((err) => console.log(err));
|
|
||||||
};
|
|
||||||
|
|
||||||
return <InfoEditForm onSubmit={handleSubmit} />;
|
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,5 @@
|
|||||||
import { PasswordChangeForm } from '../../components/PasswordChangeForm';
|
import { PasswordChangeForm } from '../../components/PasswordChangeForm';
|
||||||
import { useAuth } from '../../hooks/api/useAuth';
|
|
||||||
|
|
||||||
export default function PasswordChangePage() {
|
export default function PasswordChangePage() {
|
||||||
// TODO: 400에러 고치기
|
return <PasswordChangeForm />;
|
||||||
const { updatePassword } = useAuth();
|
|
||||||
const handleSubmit = async (currentPw, newPw, newPwCheck) => {
|
|
||||||
console.log(currentPw, newPw);
|
|
||||||
await updatePassword(currentPw, newPw, newPwCheck);
|
|
||||||
};
|
|
||||||
return <PasswordChangeForm onSubmit={handleSubmit} />;
|
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,11 @@
|
|||||||
import { useQuizsetDetail } from '../../hooks/api/useQuizsetDetail';
|
import { useQuizsetDetail } from '../../hooks/api/useQuizsetDetail';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { QuizsetDetail } from '../../components/QuizsetDetail';
|
import { QuizsetDetail } from '../../components/QuizsetDetail';
|
||||||
import { useQuizsetDelete } from '../../hooks/api/useQuizsetDelete';
|
|
||||||
|
|
||||||
export default function QuizsetDetailPage() {
|
export default function QuizsetListPage() {
|
||||||
const navigate = useNavigate();
|
const { lectureId } = useParams();
|
||||||
const { quizsetId } = useParams();
|
const { data } = useQuizsetDetail(lectureId);
|
||||||
const { quizsetDelete } = useQuizsetDelete();
|
const quizset = data?.data ?? [];
|
||||||
const { data } = useQuizsetDetail(quizsetId);
|
|
||||||
const quizset = data.data;
|
|
||||||
console.log(quizset);
|
console.log(quizset);
|
||||||
const handleDelete = async () => {
|
return <QuizsetDetail title={quizset.title} />;
|
||||||
await quizsetDelete(quizsetId);
|
|
||||||
navigate('..');
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<QuizsetDetail
|
|
||||||
topic={'퀴즈 목록'}
|
|
||||||
title={quizset.title}
|
|
||||||
quizzes={quizset.quizzes}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,47 +1,26 @@
|
|||||||
import { QuizsetForm } from '../../components/QuizForm';
|
import { QuizsetForm } from '../../components/QuizForm';
|
||||||
import { useQuizsetWrite } from '../../hooks/api/useQuizsetWrite';
|
import { useQuizsetWrite } from '../../hooks/api/useQuizsetWrite';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
export default function QuizsetWritePage() {
|
export default function QuizsetWritePage() {
|
||||||
const navigate = useNavigate();
|
// TODO: lecture에서 이미지 전송 성공 후 해당 방법으로 이미지 파일 입력
|
||||||
const { quizsetWrite } = useQuizsetWrite();
|
const { quizsetWrite } = useQuizsetWrite();
|
||||||
|
const handleSubmit = async (e, title, quizzes, imageFile = null) => {
|
||||||
const handleSubmit = async (e, title, quizzes) => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
console.log(quizzes)
|
|
||||||
|
|
||||||
const images = [];
|
|
||||||
const quizContents = [];
|
|
||||||
|
|
||||||
quizzes.forEach((quiz) => {
|
|
||||||
const { imageFile, ...quizData } = quiz;
|
|
||||||
images.push(imageFile);
|
|
||||||
quizContents.push(quizData);
|
|
||||||
});
|
|
||||||
|
|
||||||
const quizsetObject = {
|
const quizsetObject = {
|
||||||
title,
|
title,
|
||||||
quizzes: quizContents,
|
quizzes,
|
||||||
};
|
};
|
||||||
|
console.log(quizsetObject);
|
||||||
|
console.log(imageFile);
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append(
|
formData.append('quizSetCreateRequest', new Blob([JSON.stringify(quizsetObject)], { type: 'application/json' }));
|
||||||
'quizSetCreateRequest',
|
if (imageFile) {
|
||||||
new Blob([JSON.stringify(quizsetObject)], { type: 'application/json' })
|
formData.append('image', imageFile);
|
||||||
);
|
}
|
||||||
|
const response = await quizsetWrite(formData);
|
||||||
images.forEach((imageFile) => {
|
console.log(response);
|
||||||
if (imageFile) {
|
|
||||||
formData.append('images', imageFile);
|
|
||||||
} else {
|
|
||||||
formData.append('images', new Blob([''], { type: 'image/jpg' }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await quizsetWrite(formData);
|
|
||||||
navigate('..');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QuizsetForm
|
<QuizsetForm
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
|
@ -3,6 +3,7 @@ import { userTypeSlice } from './userTypeSlice';
|
|||||||
import { tokenSlice } from './tokenSlice';
|
import { tokenSlice } from './tokenSlice';
|
||||||
import { userNameSlice } from './userNameSlice';
|
import { userNameSlice } from './userNameSlice';
|
||||||
import { persist } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
|
import { liveSlice } from './liveSlice';
|
||||||
|
|
||||||
const useBoundStore = create(
|
const useBoundStore = create(
|
||||||
persist(
|
persist(
|
||||||
@ -10,6 +11,7 @@ const useBoundStore = create(
|
|||||||
...userTypeSlice(...a),
|
...userTypeSlice(...a),
|
||||||
...tokenSlice(...a),
|
...tokenSlice(...a),
|
||||||
...userNameSlice(...a),
|
...userNameSlice(...a),
|
||||||
|
...liveSlice(...a),
|
||||||
}),
|
}),
|
||||||
{ name: 'bound-store' }
|
{ name: 'bound-store' }
|
||||||
)
|
)
|
||||||
|
4
frontend/src/store/liveSlice.js
Normal file
4
frontend/src/store/liveSlice.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const liveSlice = (set) => ({
|
||||||
|
liveToken: null,
|
||||||
|
setLiveToken: (liveToken) => set({ liveToken }),
|
||||||
|
});
|
@ -3,7 +3,6 @@ import useBoundStore from '../../store';
|
|||||||
|
|
||||||
const instance = axios.create({
|
const instance = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_URL,
|
baseURL: import.meta.env.VITE_API_URL,
|
||||||
timeout: 1000,
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-type': 'application/json;charset=utf-8',
|
'Content-type': 'application/json;charset=utf-8',
|
||||||
},
|
},
|
||||||
@ -12,6 +11,7 @@ const instance = axios.create({
|
|||||||
|
|
||||||
instance.interceptors.request.use((config) => {
|
instance.interceptors.request.use((config) => {
|
||||||
const accessToken = useBoundStore.getState().token;
|
const accessToken = useBoundStore.getState().token;
|
||||||
|
|
||||||
console.log(accessToken);
|
console.log(accessToken);
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
config.headers.Authorization = `${accessToken}`;
|
config.headers.Authorization = `${accessToken}`;
|
||||||
@ -23,7 +23,7 @@ instance.interceptors.request.use((config) => {
|
|||||||
instance.interceptors.response.use(
|
instance.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
(error) => {
|
(error) => {
|
||||||
if (error.response.status !== 401) {
|
if (error.response.status !== 401 || error.request.responseURL.includes('/user/refresh')) {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ instance.interceptors.response.use(
|
|||||||
return instance(error.config);
|
return instance(error.config);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
useBoundStore.setState({ token: null });
|
useBoundStore.setState({ token: null, userType: null });
|
||||||
console.log(error);
|
console.log(error);
|
||||||
console.log('---로그아웃----');
|
console.log('---로그아웃----');
|
||||||
// TODO: redirect to home
|
// TODO: redirect to home
|
||||||
|
Loading…
Reference in New Issue
Block a user