const express = require('express')
const app = express() // Creates an Express application
app.listen(port, () => {
console.log(`Server is up on port ${port}!`)
})
const express = require('express')
const app = express() // Create an Express application
const publicDirectoryPath = path.join(__dirname, '../public')
app.use(express.static(publicDirectoryPath))
app.listen(port, () => {
console.log(`Server is up on port ${port}!`)
})
app.use(express.json());
app.use(express.static(path.join(__dirname, '../public')));
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat App</title>
<link rel="stylesheet" href="style.css">
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<script src="main.js"></script>
</body>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat App</title>
<link rel="stylesheet" href="style.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
</head>
<!-- header -->
<div class="bg-primary p-2 d-flex">
<h5 class="text-white">채팅방</h5>
</div>
<!-- User Login Form -->
<div class="login-container p-5">
<form method="post" class="user-login d-flex justify-content-center flex-column">
<input type="text" class="form-control mb-3" placeholder="유저 이름을 적어주세요." name="username" id="username" required>
<button type="submit" class="btn btn-primary">입장</button>
</form>
</div>
<!-- Sidebar -->
<div class="col-4">
<div class="sidebar border-end">
<div class="title p-2 bg-success bg-opacity-50">
나의 이름: <span id="user-title"></span>
</div>
<div class="user-title p-2 border-bottom">
<span id="users-tagline"></span>
</div>
<div class="users">
</div>
</div>
</div>
<!-- Main -->
<div class="col-8">
<div class="chat-container">
<!-- Active user title -->
<div class="title p-2 bg-success bg-opacity-50">
상대방 이름: <span id="active-user"> </span>
</div>
<!-- Message area -->
<div class="messages p-2"></div>
<!-- Message form -->
<div class="msg-form d-flex justify-content-center border-top align-items-center p-2 bg-success bg-opacity-50 d-none">
<form method="post" class="msgForm w-100">
<div class="d-flex">
<input type="text" class="form-control" name="message" id="message" placeholder="메시지 보내기..." required>
<button type="submit" style="min-width: 70px" class="ms-2 btn btn-success">전송</button>
</div>
</form>
</div>
</div>
const express = require('express')
const app = express() // Creates an Express application
const http = require('http')
const { Server } = require("socket.io");
const server = http.createServer(app)
const io = new Server(server);
const publicDirectoryPath = path.join(__dirname, '../public')
app.use(express.static(publicDirectoryPath))
server.listen(port, () => {
console.log(`Server is up on port ${port}!`)
})
HTTP 모듈 가져오기
const http = require('http');
Socket.io 라이브러리에서 Server 가져오기
const { Server } = require("socket.io");
HTTP서버 생성
const server = http.createServer(app);
WebSocket 서버 생성
const io = new Server(server);
server는 이전 단계에서 생성한 HTTP 서버의 인스턴스이다. socket.io는 이 HTTP 서버 위에서 WebSocket 서버를 구동하기 위해 이를 사용한다.
이 코드는 app이라는 Express 애플리케이션을 기반으로 HTTP 및 WebSocket 서버를 동시에 생성한다.
// socket events
let users = [];
io.on('connection', async socket => {
// get all users
let userData = {}
users.push(userData);
io.emit('users-data', { users });
// get message from client
socket.on('message-to-server', );
// fetch previous messages
socket.on('fetch-messages', );
socket.on('disconnect', );
});
이 코드는 Socket.io를 사용하여 실시간 웹 애플리케이션에서 소켓 이벤트를 핸들링하는 부분을 보여준다.
let users = [];
io.on('connection', async socket => {
let userData = {}
users.push(userData);
io.emit('users-data', { users });
socket.on('message-to-server', );
socket.on('fetch-messages', );
fetch-messages 이벤트는 사용자가 이전 메시지를 가져오려 할 때 발생한다. 이 역시 콜백 함수가 정의되지 않았으므로 이 이벤트를 처리하는 로직을 추가해야 한다.
socket.on('disconnect', );
const socket = io('http://localhost:4000/', {
autoConnect: false
});
socket.onAny((event, ...args) => {
console.log(event, args);
});
해당 코드는 Socket.io 클라이언트 라이브러리를 사용하여 특정 서버와 웹소켓 연결을 설정하는 것이다.
Socket.io클라이언트 초기화
const socket = io('http://localhost:4000/', {
autoConnect: false
});
io('http://localhost:4000/')
: 여기서는 Socket.io 서버가 localhost의 4000 포트에서 실행되고 있음을 나타낸다. 클라이언트는 이 서버로 연결을 시도한다.autoConnect
: false: 기본적으로 Socket.io 클라이언트는 초기화될 때 자동으로 서버에 연결을 시도한다. 하지만 autoConnect 옵션을 false로 설정하면 자동 연결을 중지하고 개발자가 명시적으로 socket.connect() 메서드를 호출할 때만 연결을 시작한다.모든 이벤트 수신 리스너 설정
socket.onAny((event, ...args) => {
console.log(event, args);
});
socket.onAny()
: Socket.io 클라이언트는 서버에서 발생하는 모든 이벤트를 감지하고 처리할 수 있다. onAny 메서드는 서버에서 보내는 모든 종류의 이벤트를 감지하기 위한 리스너를 설정하는데 사용된다.(event, ...args) => { ... }
: 이 콜백 함수는 서버에서 이벤트가 발생할 때마다 호출된다. event는 이벤트의 이름을 나타내고, ...args는 해당 이벤트와 함께 전송되는 추가 인자들을 나타낸다.console.log(event, args)
: 서버에서 수신된 이벤트의 이름과 인자들을 콘솔에 출력한다.// 전역 변수들
const chatBody = document.querySelector('.chat-body');
const userTitle = document.querySelector('#user-title');
const loginContainer = document.querySelector('.login-container');
const userTable = document.querySelector('.users');
const userTagline = document.querySelector('#users-tagline');
const title = document.querySelector('#active-user');
const messages = document.querySelector('.messages');
const msgDiv = document.querySelector('.msg-form');
몽고DB에 연결하기 위해서는 클러스터의 아이디와 비밀번호가 필요하다!
mongoose.set('strictQuery', false);
mongoose.connect(MONGO_URI) // 여기에 URI입력
.then(() => console.log('디비 연결 성공!'))
.catch(err => console.log(err))
const { default: mongoose } = require("mongoose");
const messageSchema = mongoose.Schema({
userToken: {
type: String,
required: true,
},
messages: [
{
from: {
type: String,
required: true
},
message: {
type: String,
required: true
},
time: {
type: String,
required: true
}
}
]
})
const messageModel = mongoose.model("Message", messageSchema);
module.exports = messageModel;
이 코드는 두 개의 문자열인 sender와 receiver를 가져와서 배열에 넣고, 이 배열의 항목들을 알파벳순으로 정렬한 다음에, _
를 사용하여 두 문자열을 연결한다.
[sender, receiver]
:.sort()
.join("_")
예시
sender = "Alice" 및 receiver = "Bob"
일 경우, 결과 key는 "Alice_Bob"
가 된다.sender = "Bob" 및 receiver = "Alice"
일 경우, 결과 key는 또한 "Alice_Bob"
가 된다. (정렬 때문에 순서가 바뀌었습니다.)이런 방식은 sender와 receiver의 순서와 관계없이 일관된 키를 생성하게 해준다.
누가 receiver가 되고 sender가 되던, 둘을 합쳐서 똑같은 토큰을 만들어서, 해당 토큰을 사용하여 DB에서 메시지를 가져온다.
(userToken에 해당된다.)
// login form handler
const loginForm = document.querySelector('.user-login');
loginForm.addEventListener('submit', (e) => {
e.preventDefault();
const username = document.getElementById('username');
createSession(username.value.toLowerCase());
username.value = '';
})
const createSession = async (username) => {
const options = {
method: 'Post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
}
await fetch('/session', options)
.then(res => res.json())
.then(data => {
socketConnect(data.username, data.userID);
// localStorage에 세션을 Set
localStorage.setItem('session-username', data.username);
localStorage.setItem('session-userID', data.userID);
loginContainer.classList.add('d-none');
chatBody.classList.remove('d-none');
userTitle.innerHTML = data.username;
})
.catch(err => console.error(err));
}
const socketConnect = async (username, userID) => {
socket.auth = { username, userID };
await socket.connect();
}
socket.on('users-data', ({ users }) => {
// 자신은 제거하기
const index = users.findIndex(user => user.userID === socket.id);
if (index > -1) {
users.splice(index, 1);
}
// user table list 생성하기
userTable.innerHTML = '';
let ul = `<table class="table table-hover">`;
for (const user of users) {
ul += `<tr class="socket-users"token interpolation">${user.username}', '${user.userID}')"><td>${user.username}<span class="text-danger ps-1 d-none" id="${user.userID}">!</span></td></tr>`
}
ul += `</table>`;
if (users.length > 0) {
userTable.innerHTML = ul;
userTagline.innerHTML = '접속 중인 유저';
userTagline.classList.remove('text-danger');
userTagline.classList.add('text-success');
} else {
userTagline.innerHTML = '접속 중인 유저 없음';
userTagline.classList.remove('text-success');
userTagline.classList.add('text-danger');
}
})
로그인 폼 핸들러 설정
const loginForm = document.querySelector('.user-login');
loginForm.addEventListener('submit', (e) => {
e.preventDefault();
const username = document.getElementById('username');
createSession(username.value.toLowerCase());
username.value = '';
})
세션 생성 함수
const createSession = async (username) => {
// ... (중략)
}
createSession
함수는 서버에 POST 요청을 보내어 세션을 생성하려고 시도한다.소켓 연결 함수
const socketConnect = async (username, userID) => {
socket.auth = { username, userID };
await socket.connect();
}
소켓 리스너 설정
socket.on('users-data', ({ users }) => {
// ... (중략)
})
users-data
이벤트가 발생할 때 실행된다. 주로 다른 클라이언트에서 보내진 사용자 데이터를 처리한다.이 코드는 웹 채팅 어플리케이션의 일부다. 사용자는 로그인 폼을 통해 이름을 입력하면, 서버에 세션을 생성하고, 해당 세션을 기반으로 소켓 연결을 수립한다. 다른 사용자들이 접속하거나 퇴장할 때, 접속 중인 사용자 목록이 실시간으로 업데이트된다.
io.use((socket, next) => {
const username = socket.handshake.auth.username;
const userID = socket.handshake.auth.userID;
if (!username) {
return next(new Error('Invalid username'));
}
socket.username = username;
socket.id = userID;
next();
})
let users = [];
io.on('connection', async socket => {
let userData = {
username: socket.username,
userID: socket.id
};
users.push(userData);
io.emit('users-data', { users })
});
Middleware설정
io.use((socket, next) => {
// ... (중략)
});
사용자 연결 핸들링
let users = [];
io.on('connection', async socket => {
// ... (중략)
});
전반적으로, 이 코드는 사용자가 소켓 서버에 연결할 때 사용자 이름과 ID를 기반으로 연결을 핸들링하는 방법을 보여준다. 연결된 사용자 목록은 실시간으로 업데이트되며, 모든 연결된 클라이언트에게 이 정보가 전송된다.
const sessUsername = localStorage.getItem('session-username');
const sessUserID = localStorage.getItem('session-userID');
if (sessUsername && sessUserID) {
socketConnect(sessUsername, sessUserID);
loginContainer.classList.add('d-none');
chatBody.classList.remove('d-none');
userTitle.innerHTML = sessUsername;
}
로컬 스토리지에서 세션 데이터 가져오기
const sessUsername = localStorage.getItem('session-username');
const sessUserID = localStorage.getItem('session-userID');
세션 데이터 확인 및 동작 실행
if (sessUsername && sessUserID) {
socketConnect(sessUsername, sessUserID);
// ... (중략)
}
요약하면, 이 코드는 웹 페이지가 로드될 때 로컬 스토리지를 확인하여 이전에 저장된 사용자 세션 데이터(사용자 이름 및 사용자 ID)가 있는지 확인한다. 만약 세션 데이터가 있다면, 해당 사용자로 자동 로그인하고 UI를 업데이트한다.
// user table list 생성하기
`<tr class="socket-users" onclick="setActiveUser(this, '${user.username}', '${user.userID}')">
<td>${user.username}<span class="text-danger ps-1 d-none" id="${user.userID}">!</span></td>
</tr>;
<tr> (table row)
: 테이블의 행을 나타내는 요소이다.class="socket-users"
: 해당 행에 "socket-users"라는 CSS 클래스가 부여되어 있다. 이를 통해 특정 스타일링 또는 JavaScript에서의 선택이 가능하게 된다.onclick="setActiveUser(this, '${user.username}', '${user.userID}')"
: 이 tr 요소를 클릭하면 setActiveUser라는 JavaScript 함수가 호출됩니다. 그 때, 이 함수는 3개의 인자를 받는다.this
: 현재 클릭된 tr 요소 자체이다.${user.username}
: 사용자의 이름. ${...} 형태는 JavaScript의 템플릿 리터럴에서 변수를 삽입하기 위한 방식이다.${user.userID}
: 사용자의 고유한 ID.<td> (table data)
: 테이블의 한 셀을 나타낸다. 여기에는 사용자의 이름과 아이콘이 포함되어 있다.${user.username}
: 사용자의 이름이 표시된다.<span class="text-danger ps-1 d-none" id="${user.userID}">!</span>
: 느낌표(!) 아이콘을 나타내는 부분이다.class="text-danger ps-1 d-none"
: 여러 CSS 클래스가 적용되어 있다.text-danger
: 텍스트의 색상을 "위험" 색상(보통 빨간색)으로 표시하기 위한 클래스다.ps-1
: 패딩 스타일이나 여백을 조절하는 클래스로 보인다.d-none
: 이 클래스가 적용되면 요소는 화면에서 숨겨진다. (일반적으로 display: none; 스타일이 적용됨)id="${user.userID}"
: 해당 span 요소에 사용자의 고유 ID를 id 속성 값으로 부여한다.const setActiveUser = (element, username, userID) => {
title.innerHTML = username;
title.setAttribute('userID', userID);
// 사용자 목록 활성 및 비활성 클래스 이벤트 핸들러
const lists = document.getElementsByClassName('socket-users');
for (let i = 0; i < lists.length; i++) {
lists[i].classList.remove('table-active');
}
element.classList.add('table-active');
// 사용자 선택 후 메시지 영역 표시
msgDiv.classList.remove('d-none');
messages.classList.remove('d-none');
messages.innerHTML = '';
socket.emit('fetch-messages', { receiver: userID });
const notify = document.getElementById(userID);
notify.classList.add('d-none');
}
해당 JavaScript 코드는 웹 페이지에서 특정 사용자를 "활성 사용자"로 설정하는 기능을 제공한다.
setActiveUser 함수 정의
const setActiveUser = (element, username, userID) => {
title.innerHTML = username;
title.setAttribute('userID', userID);
함수의 시작 부분에서는 title이라는 DOM 요소의 내부 텍스트를 해당 사용자의 이름으로 설정하고, userID 속성에 해당 사용자의 ID를 설정한다.
모든 사용자 목록의 활성화 상태 제거
// 사용자 목록 활성 및 비활성 클래스 이벤트 핸들러
const lists = document.getElementsByClassName('socket-users');
for (let i = 0; i < lists.length; i++) {
lists[i].classList.remove('table-active');
}
현재 선택된 사용자 활성화
element.classList.add('table-active');
메시지 영역 표시
// 사용자 선택 후 메시지 영역 표시
msgDiv.classList.remove('d-none');
messages.classList.remove('d-none');
messages.innerHTML = '';
이전 메시지 요청
socket.emit('fetch-messages', { receiver: userID });
알림 숨기기
const notify = document.getElementById(userID);
notify.classList.add('d-none');
요약하면, setActiveUser 함수는 웹 페이지에서 특정 사용자를 활성화 상태로 설정하고, 그에 따라 메시지 영역을 표시하며, 이전 메시지를 요청하고, 새 메시지 알림을 숨기는 기능을 수행한다.
const appendMessage = ({ message, time, background, position }) => {
let div = document.createElement('div');
div.classList.add('message', 'bg-opacity-25', 'm-2', 'px-2', 'py-1', background, position);
div.innerHTML = `<span class="msg-text">${message}</span> <span class="msg-time"> ${time}</span>`;
messages.append(div);
messages.scrollTo(0, messages.scrollHeight);
}
const msgForm = document.querySelector('.msgForm');
const message = document.getElementById('message');
msgForm.addEventListener('submit', (e) => {
e.preventDefault();
const to = title.getAttribute('userID');
const time = new Date().toLocaleString('en-US', {
hour: 'numeric',
minute: 'numeric',
hour12: true
})
// 메시지 payload 만들기
const payload = {
from: socket.id,
to,
message: message.value,
time
}
socket.emit('message-to-server', payload);
appendMessage({ ...payload, background: 'bg-success', position: 'right' });
message.value = '';
message.focus();
})
socket.on('message-to-client', ({ from, message, time }) => {
const receiver = title.getAttribute('userID');
const notify = document.getElementById(from);
if (receiver === null) {
notify.classList.remove('d-none');
} else if (receiver === from) {
appendMessage({
message,
time,
background: 'bg-secondary',
position: 'left'
})
} else {
notify.classList.remove('d-none');
}
})
위 코드는 웹 기반의 채팅 애플리케이션의 일부이며, 사용자가 메시지를 보내고 받는 기능을 처리한다.
socket.emit
를 사용하여 서버에 메시지 정보(payload)를 전송한다.appendMessage
함수를 호출하여 사용자 인터페이스에 보낸 메시지를 표시한다.요약하면, 위 코드는 웹 채팅 애플리케이션에서 사용자가 메시지를 보내고 받을 때의 동작을 처리한다. 사용자가 메시지를 보내면 해당 메시지는 서버에 전송되며, 서버로부터 메시지를 받으면 해당 메시지는 사용자 인터페이스에 표시된다.
if (receiver === null) {
notify.classList.remove('d-none');
} else if (receiver === from) {
appendMessage({
message,
time,
background: 'bg-secondary',
position: 'left'
})
} else {
notify.classList.remove('d-none');
}
해당 코드가 어떤 기능을 하는 지 자세히 보자.
변수 확인
if (receiver === null) {
새로운 메시지 알림
notify.classList.remove('d-none');
메시지 표시
} else if (receiver === from) {
appendMessage({
message,
time,
background: 'bg-secondary',
position: 'left'
})
}
그 외의 경우
} else {
notify.classList.remove('d-none');
}
이 코드는 수신된 메시지를 기반으로 사용자에게 알림을 보여주거나 채팅창에 메시지를 추가하는 로직을 수행한다.
빨간색 느낌표로 notify가 나왔다.
const messageModel = require("../models/messages.model");
const getToken = (sender, receiver) => {
const key = [sender, receiver].sort().join("_");
return key;
}
const saveMessages = async ({ from, to, message, time }) => {
const token = getToken(from, to);
const data = {
from, message, time
}
//
messageModel.updateOne({ userToken: token }, {
$push: { message: data }
}, (err, res) => {
if (err) console.error(err);
console.log('메시지가 업데이트되었습니다.');
})
}
위 코드는 데이터베이스에 메시지를 저장하기 위한 로직을 포함하고 있다.
const messageModel = require("../models/messages.model");
getToken함수
const getToken = (sender, receiver) => {
const key = [sender, receiver].sort().join("_");
return key;
}
(_)
로 연결하여 반환한다.saveMessages함수
const saveMessages = async ({ from, to, message, time }) => {
const token = getToken(from, to); // 함수로 토큰 생성
const data = {
from, message, time
}
messageModel.updateOne({ userToken: token }, {
$push: { message: data }
}, (err, res) => {
if (err) console.error(err);
console.log('메시지가 업데이트되었습니다.');
})
}
이 코드의 주된 기능은 메시지를 데이터베이스에 저장하는 것이다. 사용자 간의 메시지는 userToken이라는 고유한 키를 기반으로 구성되며, 메시지는 해당 키의 배열에 추가된다.
위 메세지 DB 배열에 메세지를 하나 넣어주는 것이 updateOne의 기능이다.
// 클라이언트에서 보내온 메시지 A ==> Server ===> B
socket.on('message-to-server', (payload) => {
io.to(payload.to).emit('message-to-client', payload); // 클라이언트에게 메세지 페이로드를 보냄
saveMessages(payload); // 메세지 페이로드를 받아 서버에 저장
})
이벤트 리스너 설정
socket.on('message-to-server', (payload) => {
메시지 전달
io.to(payload.to).emit('message-to-client', payload);
메세지 저장
saveMessages(payload);
지금까지는 채팅방을 나가는 로직이 없어, 새로고침을 하면 위처럼 자신이 계속 추가가 되었었다.
이제 채팅방을 나가는 로직을 구현하여, 새로고침을 해도 자신이 계속 추가되지 않도록 해보자.
socket.on('user-away', userID => {
const to = title.getAttribute('userID');
if (to === userID) {
title.innerHTML = ' ';
msgDiv.classList.add('d-none');
messages.classList.add('d-none');
}
})
이 코드는 Socket.io를 사용하여 클라이언트에서 특정 사용자가 채팅에서 떠난 것을 처리하는 로직이다.
이벤트 리스너 설정
socket.on('user-away', userID => {
현재 활성화된 사용자 확인
const to = title.getAttribute('userID');
활성 사용자와 떠난 사용자의 ID가 일치하는지 확인
if (to === userID) {
UI 업데이트
title.innerHTML = ' ';
msgDiv.classList.add('d-none');
messages.classList.add('d-none');
title.innerHTML = ' ';
: 현재 활성화된 채팅 상대의 이름/제목을 삭제한다. 는 HTML에서 공백을 의미한다.msgDiv.classList.add('d-none');
: 메시지 입력 영역을 숨긴다. 여기서 'd-none'은 일반적으로 Bootstrap과 같은 프레임워크에서 사용되는 클래스로, 해당 요소를 숨기는데 사용된다.messages.classList.add('d-none');
: 메시지 표시 영역을 숨긴다.새로고침을 여러 번 했는데도, 자기 자신의 아이디가 더 추가되지 않는다.
// 유저가 방에서 나갔을 때
socket.on('disconnect', () => {
users = users.filter(user => user.userID !== socket.id);
// 사이드바 리스트에서 없애기
io.emit('users-data', { users });
// 대화 중이라면 대화창 없애기
io.emit('user-away', socket.id);
});
이 코드는 Socket.io를 사용하여 클라이언트의 연결이 끊어졌을 때(예: 사용자가 채팅을 종료하거나 인터넷 연결이 끊어진 경우) 수행될 작업들을 정의하고 있다.
이벤트 리스너 설정
socket.on('disconnect', () => {
연결이 끊어진 사용자 제거
users = users.filter(user => user.userID !== socket.id);
사이드바 사용자 목록 업데이트
io.emit('users-data', { users });
대화 중인 사용자 알림
io.emit('user-away', socket.id);
위 코드는 클라이언트의 연결이 끊어졌을 때 사용자 목록을 업데이트하고, 해당 사용자와 대화 중이던 다른 사용자들에게 이 사실을 알려주는 로직을 정의하고 있다.
const setActiveUser = (element, username, userID) => {
title.innerHTML = username;
title.setAttribute('userID', userID);
const lists = document.getElementsByClassName('socket-users');
for (let i = 0; i < lists.length; i++) {
lists[i].classList.remove('table-active');
}
element.classList.add('table-active');
// 사용자 선택 후 메시지 영역 표시
msgDiv.classList.remove('d-none');
messages.classList.remove('d-none');
messages.innerHTML = '';
socket.emit('fetch-messages', { receiver: userID });
const notify = document.getElementById(userID);
notify.classList.add('d-none');
}
socket.on('stored-messages', ({ messages }) => {
if (messages.length > 0) {
messages.forEach(msg => {
const payload = {
message: msg.message,
time: msg.time
}
if (msg.from === socket.id) {
appendMessage({
...payload,
background: 'bg-success',
position: 'right'
})
} else {
appendMessage({
...payload,
background: 'bg-secondary',
position: 'left'
})
}
})
}
})
setActiveUser 함수
title.innerHTML = username;
:title.setAttribute('userID', userID);
:const lists = ...
반복문:element.classList.add('table-active');
:msgDiv.classList.remove('d-none'); ...
:socket.emit('fetch-messages', { receiver: userID });
:const notify = ...
:socket.on('stored-messages', ...)
이벤트 핸들러:if (messages.length > 0) ...
:(position: 'right')
에 녹색 배경(background: 'bg-success')
으로 표시한다.(position: 'left')
에 회색 배경(background: 'bg-secondary')
으로 표시한다.const fetchMessages = async (io, sender, receiver) => {
let token = getToken(sender, receiver);
const foundToken = await messageModel.findOne({ userToken: token });
if (foundToken) {
io.to(sender).emit('stored-messages', { message: foundToken.messages })
} else {
let data = {
userToken: token,
messages: []
}
const message = new messageModel(data);
const savedMessage = await message.save();
if (savedMessage) {
console.log('메시지가 생성되었습니다.');
} else {
console.log('메시지 생성 중 에러가 발생했습니다.');
}
}
}
// 데이터베이스에서 메시지 가져오기
socket.on('fetch-messages', ({ receiver }) => {
fetchMessages(io, socket.id, receiver);
})
.message.left {
margin-right: 20% !important;
border-top-left-radius: 0 !important;
}
.message.right {
margin-left: 20% !important;
border-bottom-right-radius: 0 !important;
}
.msg-time {
display: block;
opacity: .7;
font-size: .7rem;
}
.messages {
height: calc(80vh + 11px);
overflow-y: auto;
}
.sidebar {
height: calc(80vh + 112px);
}
.table tr td {
cursor: pointer;
}