feat: 채팅 기능 추가 (임시)

This commit is contained in:
jhynsoo 2024-07-25 13:34:58 +09:00
parent 0c93c240ea
commit 435faa9a74
8 changed files with 209 additions and 0 deletions

View File

@ -8,6 +8,7 @@
"name": "edufocus",
"version": "0.0.0",
"dependencies": {
"@stomp/stompjs": "^7.0.0",
"@tanstack/react-query": "^5.49.2",
"axios": "^1.7.2",
"react": "^18.3.1",
@ -1309,6 +1310,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@stomp/stompjs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.0.0.tgz",
"integrity": "sha512-fGdq4wPDnSV/KyOsjq4P+zLc8MFWC3lMmP5FBgLWKPJTYcuCbAIrnRGjB7q2jHZdYCOD5vxLuFoKIYLy5/u8Pw==",
"license": "Apache-2.0"
},
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",

View File

@ -13,6 +13,7 @@
"test:run": "vitest run"
},
"dependencies": {
"@stomp/stompjs": "^7.0.0",
"@tanstack/react-query": "^5.49.2",
"axios": "^1.7.2",
"react": "^18.3.1",

View File

@ -6,6 +6,8 @@ import NotFoundPage from './pages/NotFoundPage';
import { lazy } from 'react';
import MyPageLayout from './components/Layout/MyPageLayout';
import LearningLecturesPage from './pages/LearningLecturesPage/LearningLecturesPage';
import ChatRoom from './components/ChatRoom/ChatRoom';
import { MaxWidthLayout } from './components/Layout';
const LectureLayout = lazy(async () => await import('./components/Layout/LectureLayout'));
const LearningLectureDetailPage = lazy(async () => await import('./pages/LearningLectureDetailPage'));
@ -36,6 +38,17 @@ const router = createBrowserRouter([
path: 'lecture/:lectureId/info',
element: <LectureInfoPage />,
},
{
// TODO:
path: 'chat/:lectureId',
element: (
<MaxWidthLayout>
<main>
<ChatRoom />
</main>
</MaxWidthLayout>
),
},
{
path: 'lecture/:lectureId',
element: <LectureLayout />,

View File

@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Send" clip-path="url(#clip0_881_1630)">
<path id="Icon" d="M14.6666 1.3335L7.33325 8.66683M14.6666 1.3335L9.99992 14.6668L7.33325 8.66683M14.6666 1.3335L1.33325 6.00016L7.33325 8.66683" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_881_1630">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 471 B

View File

@ -0,0 +1,77 @@
import { useEffect, useRef, useState } from 'react';
import styles from './ChatRoom.module.css';
import { chatClient } from '../../utils/chat/chatClient';
import { useParams } from 'react-router-dom';
import SendIcon from '/src/assets/icons/send.svg?react';
const USER_ID = crypto.getRandomValues(new Uint32Array(1))[0];
// TODO:
export default function ChatRoom() {
const { lectureId } = useParams();
const client = chatClient;
const [messages, setMessages] = useState([]);
const inputRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
const text = inputRef.current.value;
if (!text) {
return;
}
chatClient.publish({
destination: `/pub/message/${lectureId}`,
body: JSON.stringify({
userId: USER_ID,
name: '홍길동',
content: text,
}),
});
inputRef.current.value = '';
};
useEffect(() => {
client.onConnect = () => {
client.subscribe(`/sub/channel/${lectureId}`, (response) => {
const data = JSON.parse(response.body);
const message = data.content;
setMessages((prev) => [...prev, { id: prev.length, text: message, isMine: USER_ID === data.userId }]);
});
};
client.activate();
return () => {
client.deactivate();
};
}, [client, lectureId]);
return (
<section className={styles.room}>
<h2 className={styles.title}>채팅</h2>
<ol className={styles.bubbles}>
{messages.map((message) => (
<li
key={message.id}
className={message.isMine ? styles.my : styles.your}
>
{message.text}
</li>
))}
</ol>
<form
action="POST"
onSubmit={handleSubmit}
className={styles.form}
>
<input
type="text"
ref={inputRef}
/>
<button type="submit">
<SendIcon />
</button>
</form>
</section>
);
}

View File

@ -0,0 +1,92 @@
.room {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
height: 100%;
border: 1px solid var(--border-color);
border-radius: 16px;
box-sizing: border-box;
overflow: hidden;
}
.title {
position: sticky;
top: 0;
margin: 0;
/* padding: 16px 16px 32px; */
padding: 16px;
background: linear-gradient(0deg, var(--whiteOpacity50) 0%, var(--background) 50%);
/* background-color: var(--whiteOpacity500);
backdrop-filter: blur(16px); */
}
.bubbles {
display: flex;
flex-direction: column;
justify-content: end;
align-items: start;
gap: 8px;
list-style: none;
height: 100%;
margin: 0;
padding: 0 16px;
box-sizing: border-box;
overflow-y: auto;
white-space: nowrap;
}
.your,
.my {
display: flex;
padding: 12px 16px;
border-radius: 8px;
background-color: var(--background-secondary);
color: var(--text-color);
font-size: 16px;
line-height: 1.4;
font-weight: 500;
white-space: pre-wrap;
}
.my {
align-self: end;
background-color: var(--primary-color);
color: var(--on-primary);
}
.form {
display: flex;
gap: 16px;
padding: 16px;
margin: 16px;
background-color: var(--background);
border: 1px solid var(--border-color);
border-radius: 12px;
& > input[type='text'] {
flex-grow: 1;
padding: 8px;
border: none;
border-radius: 6px;
color: var(--text-color);
font-size: 16px;
line-height: 1.4;
font-weight: 400;
}
& > button {
display: flex;
justify-content: center;
align-items: center;
padding: 8px;
border: none;
border-radius: 6px;
font-size: 16px;
line-height: 1.4;
font-weight: 500;
cursor: pointer;
background-color: var(--background);
stroke: var(--text-color);
}
}

View File

@ -8,6 +8,7 @@ const instance = axios.create({
'Content-type': 'application/json;charset=utf-8',
'Access-Control-Allow-Origin': import.meta.env.VITE_ORIGIN,
},
withCredentials: true,
});
instance.interceptors.request.use((config) => {

View File

@ -0,0 +1,8 @@
import { Client } from '@stomp/stompjs';
export const chatClient = new Client({
brokerURL: import.meta.env.VITE_CHAT_URL,
// TODO: debug 제거
debug: (str) => console.log(str),
reconnectDelay: 5000,
});