Skip to content

Commit b5a36dd

Browse files
committed
✨ feat : Add live chat functionality
- Chat icon in the video classroom space - Multi-user enabled confirmed Related Issue : #167
1 parent 2e32d6d commit b5a36dd

File tree

9 files changed

+298
-16
lines changed

9 files changed

+298
-16
lines changed

src/api/chatRoom/URLList.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const URLList = {
2+
liveURL: 'http://43.203.66.25/api/gin/chat/stream/',
3+
};
4+
5+
export default URLList;

src/api/chatRoom/getLiveMessages.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import URLList from './URLList';
2+
import {NativeEventSource, EventSourcePolyfill} from 'event-source-polyfill';
3+
import Cookies from 'js-cookie';
4+
5+
const getLiveMessages = (
6+
scheduleId: number,
7+
onMessage: (message: string) => void
8+
) => {
9+
const token = Cookies.get('access_token');
10+
const url = `${URLList.liveURL}${scheduleId}`;
11+
12+
const EventSource = EventSourcePolyfill || NativeEventSource;
13+
const eventSource = new EventSource(url, {
14+
headers: {
15+
Authorization: `Bearer ${token}`,
16+
},
17+
});
18+
19+
eventSource.onmessage = function (event) {
20+
onMessage(event.data);
21+
};
22+
23+
eventSource.onerror = function (event) {
24+
console.error('EventSource failed:', event);
25+
};
26+
27+
return eventSource;
28+
};
29+
30+
export default getLiveMessages;

src/api/chatRoom/getMessages.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import req from '../apiUtils';
2+
3+
const getMessages = async (scheduleId: number) => {
4+
const response = await req(`/chat/messages/${scheduleId}`, 'get', 'gin');
5+
return response;
6+
};
7+
8+
export default getMessages;

src/api/chatRoom/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import getLiveMessages from './getLiveMessages';
2+
import getMessages from './getMessages';
3+
import postMessage from './postMessage';
4+
import URLList from './URLList';
5+
6+
const chatRoomAPI = {
7+
getMessages,
8+
postMessage,
9+
getLiveMessages,
10+
URLList,
11+
};
12+
13+
export default chatRoomAPI;

src/api/chatRoom/postMessage.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import req from '../apiUtils';
2+
3+
const postMessage = async (
4+
scheduleId: number,
5+
user: string,
6+
message: string
7+
) => {
8+
const formData = new FormData();
9+
formData.append('user', user);
10+
formData.append('message', message);
11+
const response = await req(
12+
`/chat/room/${scheduleId}`,
13+
'post',
14+
'gin',
15+
formData
16+
);
17+
18+
return response.data;
19+
};
20+
21+
export default postMessage;
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
'use client';
2+
3+
import {useRef, useState, useEffect} from 'react';
4+
import {useParams} from 'next/navigation';
5+
import Image from 'next/image';
6+
import {useRecoilValue} from 'recoil';
7+
import userState from '@/src/recoil/atoms/userState';
8+
import chatRoomAPI from '@/src/api/chatRoom';
9+
import getUserInfo from '@/src/api/classUser/getUserInfo';
10+
import ChatInput from '@/src/app/classes/[cId]/[mId]/components/subComponents/ChatInput';
11+
import {User} from '@/src/interfaces/user';
12+
13+
interface UserInfo extends User {
14+
id: number;
15+
}
16+
17+
const ShowMain = () => {
18+
const user = useRecoilValue(userState) as User;
19+
const [message, setMessage] = useState('');
20+
const [messages, setMessages] = useState<string[]>([]);
21+
const [chatUsers, setChatUsers] = useState<{[key: string]: User}>({});
22+
const params = useParams();
23+
const getClassId = Number(params.cId);
24+
const getScheduleId = 14;
25+
const messagesEndRef = useRef<HTMLDivElement>(null);
26+
27+
const handleGetMessages = () => {
28+
chatRoomAPI
29+
.getMessages(getScheduleId)
30+
.then(res => {
31+
if (res && 'data' in res) {
32+
const {data} = res;
33+
setMessages(data || []);
34+
} else {
35+
console.error('응답에서 data를 찾을 수 없습니다.');
36+
setMessages([]);
37+
}
38+
})
39+
.catch(error => {
40+
console.error('메시지를 가져오는 중 오류 발생:', error);
41+
});
42+
};
43+
44+
const handleGetLiveStart = () => {
45+
const eventSource = chatRoomAPI.getLiveMessages(getScheduleId, message => {
46+
setMessages(prevMessages => [...prevMessages, message]);
47+
});
48+
49+
eventSource.onerror = function (event) {
50+
console.error('EventSource failed:', event);
51+
};
52+
53+
return eventSource;
54+
};
55+
56+
const handleGetUserInfo = async (uid: number) => {
57+
const userInfo = await getUserInfo(uid, getClassId);
58+
return userInfo;
59+
};
60+
61+
useEffect(() => {
62+
if (message === '') return;
63+
chatRoomAPI
64+
.postMessage(getScheduleId, user.id.toString(), message)
65+
.then(res => {
66+
setMessage('');
67+
console.log(res);
68+
});
69+
}, [message]);
70+
71+
useEffect(() => {
72+
const fetchChatUsers = async () => {
73+
const newChatUsers: {[id: string]: UserInfo} = {};
74+
for (const msg of messages) {
75+
const [id] = msg.split(': ');
76+
if (!newChatUsers[id]) {
77+
newChatUsers[id] = await handleGetUserInfo(Number(id));
78+
}
79+
}
80+
setChatUsers(newChatUsers);
81+
};
82+
83+
fetchChatUsers();
84+
}, [messages]);
85+
86+
useEffect(() => {
87+
if (messagesEndRef.current) {
88+
messagesEndRef.current.scrollIntoView({behavior: 'smooth'});
89+
}
90+
}, [messages]);
91+
92+
useEffect(() => {
93+
const eventSource = handleGetLiveStart();
94+
return () => {
95+
eventSource.close();
96+
};
97+
}, [getScheduleId]);
98+
99+
useEffect(() => {
100+
handleGetMessages();
101+
}, [getScheduleId]);
102+
103+
return (
104+
<div className="flex justify-center mt-10">
105+
<div className="w-full max-w-md">
106+
<div className="w-full mt-2">
107+
<div className="border-4 rounded-lg border-gray-500 p-2 h-[550px] overflow-auto">
108+
<p className="text-center inline-block px-4 py-2 text-sm text-white bg-violet-300 rounded-lg w-full">
109+
최상단 채팅 내용입니다
110+
</p>
111+
{messages.map((msg, index) => {
112+
const [id, message] = msg.split(': ');
113+
const chatUser = chatUsers[id];
114+
if (!chatUser) {
115+
return null;
116+
}
117+
return (
118+
<div
119+
key={index}
120+
className={
121+
Number(id) === user.id
122+
? 'flex justify-end items-start'
123+
: 'flex justify-start items-start'
124+
}
125+
>
126+
{Number(id) !== user.id && (
127+
<div className="flex items-center mt-2">
128+
<div className="rounded-full w-8 h-8 overflow-hidden max-w-20">
129+
<Image
130+
src={chatUser.image || user.image}
131+
alt={'userImage'}
132+
width={40}
133+
height={40}
134+
/>
135+
</div>
136+
<div className="ml-2">
137+
<p>{chatUser.nickname || user.name}</p>
138+
<p
139+
className={
140+
'inline-block px-3 py-2 text-sm text-white bg-gray-500 rounded-lg max-w-72'
141+
}
142+
>
143+
{message}
144+
</p>
145+
</div>
146+
</div>
147+
)}
148+
{Number(id) === user.id && (
149+
<div className="mt-2">
150+
<p
151+
className={
152+
'inline-block px-4 py-2 text-sm text-white bg-blue-400 rounded-lg max-w-72'
153+
}
154+
>
155+
{message}
156+
</p>
157+
</div>
158+
)}
159+
</div>
160+
);
161+
})}
162+
<div ref={messagesEndRef} />
163+
</div>
164+
</div>
165+
<ChatInput setMsg={setMessage} />
166+
</div>
167+
</div>
168+
);
169+
};
170+
171+
export default ShowMain;

src/app/classes/[cId]/[mId]/components/manageSubComponents/ManageSubContainer.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import React from 'react';
1+
import React, {useState} from 'react';
22
import LiveClass from './LiveClass';
33
import {useParams} from 'next/navigation';
44
import {useRecoilValue} from 'recoil';
55
import {User} from '@/src/interfaces/user';
66
import userState from '@/src/recoil/atoms/userState';
7+
import ShowMain from '../chatComponents/page';
78

89
const ManageSubContainer: React.FC = () => {
10+
const [showMain, setShowMain] = useState(false);
911
const {cId} = useParams<{cId: string}>();
1012
const classId = parseInt(cId, 10);
1113
const user = useRecoilValue(userState) as User;
@@ -17,6 +19,17 @@ const ManageSubContainer: React.FC = () => {
1719
return (
1820
<div>
1921
<LiveClass classId={classId} userId={user.id} />
22+
<button
23+
onClick={() => setShowMain(!showMain)}
24+
className="fixed top-1 right-4 z-50 bg-primary text-primary-foreground rounded-full p-3 shadow-lg hover:bg-primary/90"
25+
>
26+
{showMain ? '💬' : '💬'}
27+
</button>
28+
{showMain && (
29+
<div className="fixed top-0 right-0 w-96 h-full bg-white shadow-lg z-10">
30+
<ShowMain />
31+
</div>
32+
)}
2033
</div>
2134
);
2235
};

src/app/classes/[cId]/[mId]/components/subComponents/SubContainer.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import PromptChat from './PromptChat';
77
import Storage from './Storage';
88
import materialState from '@/src/recoil/atoms/materialState';
99
import '@/src/styles/variable.css';
10+
import ShowMain from '../chatComponents/page';
1011

1112
const SubContainer: React.FC = () => {
1213
const TABS = ['프롬프트창', '저장목록'];
1314
const [activeTab, setActiveTab] = useState(TABS[0]);
15+
const [showMain, setShowMain] = useState(false);
1416
const params = useParams<{cId: string}>();
1517
const material = useRecoilValue(materialState);
1618

@@ -39,6 +41,17 @@ const SubContainer: React.FC = () => {
3941
<div className="subContainer">
4042
<TabsMapping activeTab={activeTab} tabMapping={tabMapping} />
4143
</div>
44+
<button
45+
onClick={() => setShowMain(!showMain)}
46+
className="fixed top-1 right-4 z-50 bg-primary text-primary-foreground rounded-full p-3 shadow-lg hover:bg-primary/90"
47+
>
48+
{showMain ? '💬' : '💬'}
49+
</button>
50+
{showMain && (
51+
<div className="fixed top-0 right-0 w-96 h-full bg-white shadow-lg z-10">
52+
<ShowMain />
53+
</div>
54+
)}
4255
</div>
4356
);
4457
};

src/app/classes/components/_class/modal/ClassJoin.tsx

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@
22

33
import React, {useState} from 'react';
44
import Image from 'next/image';
5-
import getCheckClassSecret from '@/src/api/classCode/getCheckClassSecret';
5+
import getVerifySecret from '@/src/api/classCode/getVerifySecret';
6+
import putUserName from '@/src/api/classUser/putUserName';
67
import {ModalProps} from '@/src/interfaces/_class/modal';
78
import icons from '@/public/svgs/_class';
89

9-
const ClassJoin = ({setActiveModalId, setIsModalOpen}: ModalProps) => {
10+
const ClassJoin = ({setActiveModalId, uid, name}: ModalProps) => {
1011
const [inputValue, setInputValue] = useState('');
12+
const [password, setPassword] = useState('');
1113

1214
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
1315
setInputValue(event.target.value);
1416
};
17+
const handlePasswordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
18+
setPassword(event.target.value);
19+
};
1520

1621
const handleSubmit = async (
1722
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
@@ -21,19 +26,15 @@ const ClassJoin = ({setActiveModalId, setIsModalOpen}: ModalProps) => {
2126
alert('클래스 코드를 입력해주세요');
2227
return;
2328
}
24-
try {
25-
const res = await getCheckClassSecret(inputValue);
26-
console.log(res);
27-
if (!res.secretExists) {
28-
alert('이 클래스는 비밀번호가 설정되어있지 않습니다');
29-
return;
30-
}
31-
setActiveModalId('');
32-
if (setIsModalOpen) {
33-
setIsModalOpen(true);
29+
if (uid && name) {
30+
try {
31+
const res = await getVerifySecret(inputValue, password, uid);
32+
await putUserName(uid, res.cid, name);
33+
alert('신청 완료되었습니다.');
34+
setActiveModalId('');
35+
} catch (error: unknown) {
36+
alert('잘못된 요청입니다.');
3437
}
35-
} catch (error: unknown) {
36-
alert('잘못된 요청입니다.');
3738
}
3839
};
3940

@@ -75,13 +76,20 @@ const ClassJoin = ({setActiveModalId, setIsModalOpen}: ModalProps) => {
7576
</div>
7677
<div className="mt-3">
7778
<input
78-
className="mt-3 w-full inline-flex justify-center rounded-md border ring-gray-100 shadow-sm px-4 py-2 bg-white-50 text-base font-medium hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:bg-gray-50 focus:ring-gray-100 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
79+
className="mt-3 w-full inline-flex justify-center rounded-md border ring-gray-100 shadow-sm px-4 py-2 bg-white-50 text-base font-medium hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:bg-gray-50 focus:ring-gray-100"
7980
type="text"
8081
placeholder="클래스 코드를 입력해주세요"
8182
value={inputValue}
8283
onChange={handleInputChange}
8384
required
8485
/>
86+
<input
87+
type="password"
88+
className="mt-3 w-full inline-flex justify-center rounded-md border ring-gray-100 shadow-sm px-4 py-2 bg-white-50 text-base font-medium hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:bg-gray-50 focus:ring-gray-100"
89+
placeholder="비밀번호를 입력해주세요"
90+
value={password}
91+
onChange={handlePasswordChange}
92+
/>
8593
</div>
8694
</div>
8795
</div>

0 commit comments

Comments
 (0)