채팅방 만들기 실습
✒️ 2025-05-26 14:05 내용 수정
- 수업 시간에 진행한 채팅방 만들기 실습을 정리했다.
- 회원가입, 로그인을 할 수 있다.
- 로그인한 사용자들은 글을 작성, 수정, 삭제할 수 있고, 다른 사용자들의 글을 볼 수 있다.
- 글 작성자와 1대1 채팅을 할 수 있다.
1. 환경 설정
- 실습에 사용할 라이브러리를 모두 다운 받는다.
- express, express-session : express 서버와 세션
- passport, passport-local : 로그인 기능
- bcrypt : 암호화 사용
- dotenv : 환경 설정 파일 사용
- ejs : view engine
- method-override : 메소드 오버라이드에 사용하며, HTML에서 PUT을 사용하기 위해 추가
- mongodb, connect-mongo : MongoDB 연결
- socket.io : web socket 라이브러리
npm install express express-session ejs bcrypt dotenv passport passport-local socket.io mongodb connect-mongo method-override
- package.json 파일
{
"dependencies": {
"bcrypt": "^5.1.1",
"connect-mongo": "^5.1.0",
"dotenv": "^16.4.5",
"ejs": "^3.1.9",
"express": "^4.19.2",
"express-session": "^1.18.0",
"method-override": "^3.0.0",
"mongodb": "^6.5.0",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
"socket.io": "^4.7.5"
}
}
.env파일을 만들어 실습에서 사용할 port 번호와 MongoDB URL을 설정한다.
# PORT 설정
PORT = 3000
# MongoDB 연결 주소
URL = 'mongoDB URL'
- css를 저장할 public 폴더, route 설정을 위한 routes 폴더, ejs 파일들을 저장할 views 폴더를 만든다.
2. DB 연결
- db.js에 DB 연결을 위한 connectDB 객체를 만들고, 이를 모듈로 내보낸다.
- 내보낸 객체는 각 js 파일에서 가져와서 연결 후 db를 가져오는 방식으로 사용했다.
// 환경 변수
require("dotenv").config();
// mongodb
const { MongoClient } = require("mongodb");
let connectDB = new MongoClient(process.env.URL).connect(); // process.env.URL는 .env에 있는 URL
module.exports = connectDB;
- 다른 js 파일에서 연결된 db
let connectDB = require("../db.js"); // connectDB를 가져옴
let db;
// db 연결
connectDB.then((client) => { // Promise 객체를 resolve 진행
db = client.db("forum"); // "forum" db 연결
}).catch((err) => {
console.log(err);
});
3. 서버 사이드
- server.js에서 서버 설정, 로그인, socket 설정, 라우트 설정을 진행한다.
- socket 채팅 예시 참고 자료 : Socket.io Get started
- method-override 참고 자료 : express method-override
// 서버 세팅 -------------------------------
// 환경변수
require("dotenv").config();
// method-override for REST API
const methodOverride = require("method-override");
// express 설정
const express = require("express");
const app = express();
// http와 socket.io 설정
// https://socket.io/get-started/chat
const { createServer } = require("http");
const { Server } = require("socket.io");
const exp = require("constants");
const server = createServer(app); // express를 사용한 http 서버 생성
const io = new Server(server); // socket 서버 생성
// session 및 로그인 설정
const session = require("express-session"); // session
const passport = require("passport"); // 로그인
const LocalStrategy = require("passport-local"); // 로그인방법
const bcrypt = require("bcrypt"); // 암호화
// db 설정
const MongoStore = require("connect-mongo"); // DB에 session 저장용
const { ObjectId } = require("mongodb");
// let connectDB = require("./db.js"); // 생성해둔 connection 가져옴
let connectDB = require("./db.js");
let db;
connectDB.then((client) => {
db = client.db("forum"); // "form" db 연결
server.listen(process.env.PORT, () => {
console.log(`서버 접속 가능 : http://localhost:${process.env.PORT}`);
});
}).catch((err) => {
console.log(err);
});
// session 설정
const sessionOption = {
secret : "secret-express-session", // secret 키
resave : false,
saveUninitialized : false,
cookie : { maxAge : 60 * 60 * 1000 }, // 1시간
store : MongoStore.create({ // db에 세션 저장 설정
mongoUrl : process.env.URL, // db 주소
dbName : "forum" // db 이름
})
}
// app 설정
app.use(express.static(__dirname + "/public"));
app.use(express.json());
app.use(express.urlencoded({ extended : true }));
app.use(methodOverride("_method"));
// method override 사용 | https://expressjs.com/en/resources/middleware/method-override.html
app.set("view engine", "ejs");
// 로그인 설정 - passport | 순서 반드시 지켜야 함
app.use(passport.initialize()); // 초기화, 사용자 인증 처리
app.use(session(sessionOption)); // session 옵션 사용
app.use(passport.session()); // 세션을 사용하도록 설정
passport.use(new LocalStrategy( // 로그인 방법
{usernameField : "userid", passwordField : "password"}, // 사용자이름, 비번
async (userid, password, done) => { // 로그인 처리
// db에서 userid 조회
let result = await db.collection("user").findOne({ userid });
// 아이디 없음 처리
if (!result) {
return done(null, false, { message : "fail" });
}
// 비밀번호 비교 처리
if (await bcrypt.compare(password, result.password)) {
return done(null, result);
} else {
return done(null, false, { message : "fail" });
}
}
));
passport.serializeUser( (user, done) => { // 로그인 시 실행, req.session에 데이터 저장
process.nextTick(() => {
done(null, { id : user._id, userid : user.userid });
});
});
passport.deserializeUser( async (user, done) => { // 매 요청마다 실행, id로 사용자 정보 객체 불러옴
// id로 사용자 정보 조회
// user는 passport.serializeUser에서 저장된 user
let result = await db.collection("user").findOne({ _id : new ObjectId(user.id) });
delete result.password; // 사용자 정보에서 password 정보 삭제
process.nextTick(() => {
return done(null, result); // req.user에 result 저장
});
});
// socket.io에서 세션 사용-----------------------
const sessionMiddleWare = session(sessionOption);
io.use((socket, next) => {
sessionMiddleWare(socket.request, {}, next);
});
io.on("connection", (socket) => { // listen on connection event
console.log("websocket 연결 완료");
// 채팅방 참여 요청
socket.on("ask-join", (data) => { // receiver
socket.join(data);
});
socket.on("message-send", async (data) => {
await db.collection("chatMessage").insertOne({
parentRoom : new ObjectId(data.room),
content : data.msg,
who : new ObjectId(socket.request.session.passport.user.id),
date : new Date()
});
// broadcasting
io.to(data.room).emit("message-broadcast", data.msg);
});
});
// 라우터 설정-----------------------------
// 메인 화면
app.get("/", (req, res) => {
res.redirect("/list");
});
app.use("/", require("./routes/join.js"));
app.use("/", require("./routes/login.js"));
app.use("/", require("./routes/crud.js"));
app.use("/", require("./routes/chat.js"));
4. 라우트
- join.js : 회원가입에 필요한 라우트 설정
// router 사용
const router = require("express").Router();
const bcrypt = require("bcrypt");
let connectDB = require("../db.js");
let db;
// db 연결
connectDB.then((client) => {
db = client.db("forum"); // "forum" db 연결
}).catch((err) => {
console.log(err);
});
// 회원가입 페이지 이동
router.get("/join", (req, res) => {
res.render("join.ejs", { loginUser : null });
});
// 회원가입
router.post("/join", async (req, res) => {
const { userid, password } = req.body;
let hash = await bcrypt.hash(password, 10); // 비밀번호 암호화
// 사용자 정보를 db에 추가
let result = await db.collection("user").insertOne({
userid,
password : hash
});
// 성공 및 실패 여부를 전송
if (result) {
res.json({ joinResult : "success"});
} else {
res.json({ joinResult : "fail"});
}
});
module.exports = router;
- login.js : 로그인에 필요한 라우트 설정
// login.js
// router 사용
const passport = require("passport");
const router = require("express").Router();
// 로그인 페이지 이동
router.get("/login", (req, res) => {
res.render("login.ejs", { loginUser : null });
});
// 로그인
router.post("/login", async (req, res, next) => {
passport.authenticate(("local"), (error, user, info) => {
// 인증 오류 처리
if (error) return res.json({ message : error });
// 로그인 실패 처리
if (!user) return res.json( { message : info.message });
// 에러 발생 시 next 미들웨어로 오류 처리 넘김
if (error) return next(err);
req.logIn(user, (err) => {
// 에러 발생 시 next 미들웨어로 오류 처리 넘김
if (err) return next(err);
return res.json( {message : "success" });
// res.redirect("/list");
});
})(req, res, next);
});
// 로그아웃 - DB에 저장된 세션에도 자동 처리됨
router.get("/logout", (req, res) => {
req.logOut(() => {
res.redirect("/");
});
});
module.exports = router;
- crud.js : 글 조회, 작성, 수정, 제거에 필요한 라우트 설정
// router 사용
const router = require("express").Router();
const { ObjectId } = require("mongodb");
let connectDB = require("../db.js");
let db;
// db 연결
connectDB.then((client) => {
db = client.db("forum"); // "forum" db 연결
}).catch((err) => {
console.log(err);
});
// 글 조회
router.get("/list", async (req, res) => {
const { title } = req.query;
const loginUser = req.user ? req.user.userid : "unknown";
// 제목 포함된 글 찾기
if (title) {
let result = await db.collection("post").find({
title : { $regex : new RegExp(title) }
}).toArray();
res.render("list.ejs", { list : result });
} else {
let result = await db.collection("post").find().toArray();
res.render("list.ejs", { list : result, loginUser });
}
});
// 상세보기
router.get("/detail/:id", async (req, res) => {
const { id } = req.params;
const loginUser = req.user ? req.user.userid : "unknown";
try {
// DB에서 id로 특정 글 검색
let result = await db.collection("post").findOne({
_id : new ObjectId(id)
});
// 특정 글의 댓글 검색
let comment = await db.collection("comment").find({
parentId : id
}).toArray();
if (!result) {
res.status(400).send("글을 찾을 수 없습니다");
} else {
res.render("detail.ejs", { result, comment, loginUser});
}
} catch (error) {
console.log(error);
}
});
// 글 작성 페이지로 이동
router.get("/add", async (req, res) => {
// 로그인해야만 글 작성이 가능하므로 req.user에 값이 있는지 확인해야 함
if (!req.user) {
return res.redirect("/login");
}
res.render("write.ejs", { loginUser : req.user.userid });
});
// 글 작성
router.post("/add", async (req, res) => {
const { title, content } = req.body;
const { id, userid } = req.user;
try {
// 글 작성 유효성 검사
if (title == '') {
res.send("제목을 입력해주세요");
} else if (title.length > 20) {
res.send("제목은 20글자 이하만 가능합니다");
} else if (content == '') {
res.send("내용을 입력해주세요");
} else {
// 검사 통과 시 글 추가
let result = await db.collection("post").insertOne({
title,
content,
userNum : id,
userid : userid
}, (error, r) => {
console.log("글 추가 완료");
});
res.redirect("/list");
}
} catch (err) {
console.log(err);
res.send("글 수정 실패 : DB 에러 발생");
}
});
// comment 추가
router.post("/comment", async (req, res) => {
const { content, parentId } = req.body;
const writerId = req.user.id;
const writer = req.user.userid;
// 댓글 추가
let result = await db.collection("comment").insertOne({
content,
parentId,
writerId,
writer
});
// 이전 페이지로 돌아가기
res.redirect("back");
});
// 글 수정 페이지 이동
router.get("/edit/:id", async (req, res) => {
const { id } = req.params;
const loginUser = req.user ? req.user.userid : null;
let result;
if (loginUser) { // 로그인 진행때만 수행. 작성자와 로그인한 계정 검사는 추가 못함
result = await db.collection("post").findOne({
_id : new ObjectId(id)
});
} else {
res.redirect("/login");
}
res.render("edit.ejs", { result, loginUser });
});
// 글 수정
router.put("/edit", async (req, res) => {
const { id, title, content } = req.body;
try {
// 사용자 로그인 여부 및 작성자와 일치 여부는 추가 못함
// 글 수정 진행
let result = await db.collection("post").updateOne(
{ _id : new ObjectId(id) },
{ $set : { title, content } }
);
res.redirect("/list");
} catch (error) {
console.log(error);
}
});
// 글 삭제
router.delete("/delete/:id", async (req, res) => {
const post_id = req.params.id;
const userNum = req.user.id;
try {
// 글 삭제 진행
let result = await db.collection("post").deleteOne({
_id : new ObjectId(post_id),
userNum : userNum
});
const { deleteCount } = result;
if (deleteCount == 0) {
res.send("글 삭제 실패");
}
// list.ejs에서 fetch로 delete를 요청했기에 redirect 수행 x
res.send("제거 완료");
} catch (error) {
console.log(error);
}
});
module.exports = router;
- chat.js : 채팅에 필요한 라우트 설정
// router 사용
const router = require("express").Router();
const bcrypt = require("bcrypt");
const { ObjectId } = require("mongodb");
let connectDB = require("../db.js");
let db;
// db 연결
connectDB.then((client) => {
db = client.db("forum"); // "forum" db 연결
}).catch((err) => {
console.log(err);
});
// 채팅방 조회
router.get("/chat/list", async (req, res) => {
const loginUser = req.user ? req.user.userid : null;
const loginUserid = req.user ? req.user.id : null;
if (req.user) { // 로그인 시에만 채팅방 목록 조회
let result = await db.collection("chatroom").find({
// 채팅방 참여자에 로그인한 사용자 정보가 있는 경우를 조건으로 추가
member : { $in : [
loginUserid,
new ObjectId(loginUserid),
]}
}).toArray();
res.render("chatList.ejs", { result, loginUser });
} else {
res.redirect("/login");
}
});
// 채팅 요청
router.get("/chat/request", async (req, res) => {
const loginUserid = req.user ? req.user.id : null;
// 글 작성자와 현재 로그인한 유저가 속한 채팅방 조회
let result = await db.collection("chatroom").find({
member : { $all : [loginUserid, new ObjectId(req.query.writerId)]}
}).toArray();
if (result.length == 0) { // 채팅방이 없다면 새로 추가
await db.collection("chatroom").insertOne({
member : [loginUserid, new ObjectId(req.query.writerId)],
date : new Date()
});
}
res.redirect("/chat/list");
});
// 채팅방 내용 확인
router.get("/chat/detail/:id", async (req, res) => {
const { id } = req.params;
const loginUser = req.user ? req.user.userid : null;
const loginUserid = req.user ? req.user.id : null;
if (loginUser) { // 로그인 시에만 채팅 내역 확인 가능
// 채팅방 조회
let result = await db.collection("chatroom")
.findOne({ _id : new ObjectId(id) });
// 채팅 참여자 조회
let member = await db.collection("user")
.find({ _id : { $in : [
new ObjectId(result.member[0]),
new ObjectId(result.member[1].toString()),
]}}).toArray();
member.map((el) => { // 조회한 사용자 정보 중 비밀번호는 제거함
return delete el.password;
});
// 채팅 참여 인원 외의 다른 사람 접근 차단
if (loginUserid != member[0]._id.toString() && loginUserid != member[1]._id.toString() ) {
return;
}
// 채팅 내역 조회
let chat = await db.collection("chatMessage")
.find({ parentRoom : new ObjectId(id) }).toArray();
res.render("chatDetail.ejs", { result, chat, member, loginUser, loginUserid });
} else {
res.redirect("/login");
}
});
module.exports = router;
5. views(ejs)
- nav.ejs : header와 네비게이션 요소를 저장한 파일
- 사용자가 로그인한 상태라면 로그아웃을, 로그인을 안 한 상태라면 회원가입과 로그인을 표시한다.
<header class="header">
<div class="inner">
<h1><a class="logo" href="/">Forum</a></h1>
<nav class="nav">
<% if (!loginUser || loginUser == "unknown") { %>
<a href="/join">join</a>
<a href="/login">login</a>
<% } else { %>
<span class="username">welcome! <%= loginUser %></span>
<a href="/logout">logout</a>
<% } %>
<a href="/add">write</a>
<a href="/chat/list">chatroom</a>
</nav>
</div>
</header>
- join.ejs : 회원가입 페이지
- 아이디와 비밀번호가 비어있는지 확인하는 유효성 검사를 진행하며, 회원가입 성공 시에는 바로 로그인 페이지로 이동한다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>register</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<%- include('nav.ejs') %>
<section class="sec">
<h2>회원가입</h2>
<form class="form-box">
<input name="userid" placeholder="userid" value="">
<input name="password" placeholder="password" type="password" value="">
<button class="confirm">확인</button>
</form>
</section>
<!-- ajax를 쉽게 사용하기 위해 jquery를 사용했다 -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
const $form = $(".form-box");
$(".confirm").on("click", function (event) {
event.preventDefault(); // form 제출 방지. 원래는 form을 사용했다가 변경했다
let $userid = $("input[name='userid']").val();
let $password = $("input[name='password']").val();
// 유효성 검사
if(!$userid) {
alert("아이디를 입력해주세요");
return;
} else if (!$password) {
alert("비밀번호를 입력해주세요");
return;
}
// ajax로 데이터를 전송하고 그 결과로 alert 창을 띄움
$.ajax({
url : "/join",
type : "POST",
data : JSON.stringify({'userid' : $userid, 'password' : $password}),
dataType : "json",
contentType : "application/json; charset=utf-8",
success: function(data) {
if(data["joinResult"]) {
alert("성공적으로 회원가입 되었습니다!");
window.location.href = "/login";
} else {
alert("회원가입을 실패했습니다. 다시 시도해주세요");
return;
}
}
});
})
</script>
</body>
</html>
- login.ejs : 로그인 페이지
- 아이디와 비밀번호를 입력할 때 유효성 검사를 진행하며, 아이디나 비밀번호가 일치하지 않으면 오류 메시지를 표시한다.
- bootstrap으로도 가능하지만 현 실습에선 jquery의
addClass(className)과html(element)함수를 사용해서 구현했다.
- bootstrap으로도 가능하지만 현 실습에선 jquery의
- 아이디와 비밀번호를 입력할 때 유효성 검사를 진행하며, 아이디나 비밀번호가 일치하지 않으면 오류 메시지를 표시한다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>login</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<%- include('nav.ejs') %>
<section class="sec">
<h2>로그인</h2>
<form class="form-box">
<input name="userid" placeholder="userid" value="">
<input name="password" placeholder="password" type="password" value="">
<button class="confirm">확인</button>
<div class="error-box"></div>
</form>
</section>
<!-- ajax를 쉽게 사용하기 위해 jquery를 사용했다 -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
const $form = $(".form-box");
let $error = $(".error-box");
const message = "<p>아이디가 존재하지 않거나 비밀번호가 일치하지 않습니다.<br>아이디와 비밀번호를 다시 확인해주세요</p>";
$(".confirm").on("click", function (event) {
event.preventDefault();
let $userid = $("input[name='userid']").val();
let $password = $("input[name='password']").val();
// 유효성 검사
if(!$userid) {
alert("아이디를 입력해주세요");
return;
} else if (!$password) {
alert("비밀번호를 입력해주세요");
return;
}
// ajax
$.ajax({
url : "/login",
type : "POST",
data : JSON.stringify({'userid' : $userid, 'password' : $password}),
dataType : "json",
contentType : "application/json; charset=utf-8",
success : function (data) {
console.log(data)
if (data["message"] == "success") {
window.location.href ="/";
$error.removeClass("fail");
} else {
$error.addClass("fail");
$(".fail").html(message);
return;
}
}
});
})
</script>
</body>
</html>
- list.ejs : 글 목록 페이지
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>list</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<%- include('nav.ejs') %>
<section class="sec">
<h2> 글 목록 </h2>
<div class="white-bg">
<% if ( list.length == 0 ) {%>
<p> 새로운 글을 작성해주세요. 👉 <a href="/add" style="list-style:underline">글쓰기</a></p>
<% } else { %>
<% for(let i = 0; i < list.length; i++) { %>
<% let checkLogin = loginUser == list[i].userid %>
<% let className = (checkLogin)? 'writer login' : 'writer' %>
<div class="list-box">
<a href="/detail/<%= list[i]._id %>">
<h3><%= list[i].title %></h3>
<p><%= list[i].content %></p>
<p class="<%= className %>"><%= list[i].userid %></p>
</a>
<% if(checkLogin) { %>
<div class="btn">
<a href="/edit/<%= list[i]._id %>">✏️</a>
<a href="/delete/<%= list[i]._id %>" class="delete" data-id="<%= list[i]._id %>" >🗑️</a>
</div>
<% } %>
</div>
<% } } %>
</div>
</section>
<script>
const BASE_URL = 'http://localhost:3000'
const del = document.querySelectorAll('.delete');
del.forEach((el, i)=>{
el.addEventListener('click', async function(e){
e.preventDefault();
const id = e.target.dataset.id
// 새로고침을 안하고 동작
const response = await fetch(`${BASE_URL}/delete/${id}`, {
method: 'DELETE',
});
console.log(response)
e.target.parentElement.parentElement.style.display = 'none'
})
})
</script>
</body>
</html>
- write.ejs : 글 작성 페이지
- crud.js에서 로그인을 안한 사용자는 로그인 페이지로 이동하게끔 처리했다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>write</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<%- include('nav.ejs') %>
<section class="sec">
<h2>글 쓰기</h2>
<form class="form-box" action="/add" method="POST">
<input name="title" placeholder="title">
<input name="content" placeholder="content">
<button>전송</button>
</form>
</section>
</body>
</html>
- detail.ejs : 글 상세보기 페이지
- 글 작성자라면 수정과 삭제 아이콘이 뜨며, 작성자가 아니라면 보이지 않는다.
- 상세보기에서 댓글을 작성할 수 있고, 로그인을 안 한 상태라면 댓글을 작성할 수 없다.
- 글 작성자와 1대1 채팅하기는 로그인한 사용자만 할 수 있고(chat.js에서 해당 부분을 처리), 처음 1대1 채팅을 요청하는 경우엔 새 채팅방을 만든다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>post</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<%- include('nav.ejs') %>
<section class="sec">
<div class="white-bg">
<div class="list-box">
<h3><%= result.title %></h3>
<p class="writer">
작성자 : <%= result.userid %>
<% let checkLogin = loginUser == result.userid %>
<% if(checkLogin) { %>
<span class="btn">
<a href="/edit/<%= result._id %>">✏️</a>
<a href="/delete/<%= result._id %>" class="delete" data-id="<%= result._id %>" >🗑️</a>
</span>
<% } %>
</p>
<!-- writerId : 작성자 id, reslut._id : 현재 로그인한 사용자 아이디 -->
<% if(!checkLogin) { %>
<a href="/chat/request?writerId=<%= result._id %>">채팅하기</a>
<% } %>
<p><%= result.content %></p>
</div>
<!-- parentId : 게시글번호 (post컬렉션의 _id)-->
<div class="comment-write">
<% if(loginUser == 'unknown'){ %>
<a href="/login">로그인</a> 해주세요
<% }else{ %>
<form action="/comment" method="POST" id="comment">
<label for="commentContent"><%= loginUser %> </label>
<input name="content" id="commentContent">
<input name="parentId" value="<%= result._id %>" type="hidden">
<button>댓글작성</button>
</form>
<% } %>
</div>
<!-- 댓글목록 -->
<div class="comment-list">
<% for(let i = 0; i < comment.length; i++){ %>
<p>
<span><%= comment[i].writer %> </span>
<span><%= comment[i].content %></span>
</p>
<% } %>
</div>
</div>
</section>
</body>
</html>
- edit.ejs : 글 수정 페이지
- 글을 수정할 수 있으며, HTML의 form에는 PUT을 사용할 수 없기에 route에 있는 PUT에 해당 내용을 전송하기 위해 method-override를 설정했다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>write</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<%- include('nav.ejs') %>
<section class="sec">
<h2>글수정</h2>
<!-- https://expressjs.com/en/resources/middleware/method-override.html -->
<form class="form-box" action="/edit?_method=PUT" method="POST">
<input class="inputId" name="id" value="<%= result._id%>" type="hidden">
<input class="inputTitle" name="title" placeholder="title" value="<%= result.title%>">
<input class="inputContent" name="content" placeholder="content" value="<%= result.content%>">
<button class="edit">전송</button>
</form>
</section>
</body>
</html>
- chatList.ejs : 채팅방 목록 페이지
- 현재 로그인한 사용자가 속한 채팅방 목록을 표시한다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>list</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<%- include('nav.ejs') %>
<section class="sec">
<h2> 채팅 목록 </h2>
<div class="white-bg">
<% for (let i = 0; i < result.length; i++){ %>
<div class="list-box">
<!-- id : chatroom의 _id 고유번호 -->
<a href="/chat/detail/<%= result[i]._id%>">
<h3><%= result[i].member[0]%></h3>
<p><%= result[i]._id%></p>
<p>test</p>
</a>
</div>
<% } %>
</div>
</section>
</body>
</html>
-
아래 사진은 apple 사용자와 tester 사용자가 각각 본인이 속한 채팅방 목록이다.
- 서로 같은 1대1 채팅방에 있기에 같은 방이 뜬다.
- 서로 같은 1대1 채팅방에 있기에 같은 방이 뜬다.
-
chatDetail.ejs : 채팅방 페이지
- DB에 저장된 채팅 데이터에서 작성된 날짜를 한국 시간으로 표기하고, 같은 날에 올라온 채팅은 날짜별로 표시한다.
- 채팅을 보낸 시간은 메시지 옆에 표시한다.
- 현재 로그인한 사용자의 채팅이라면 왼쪽에, 다른 사용자의 채팅이라면 오른쪽에 표시한다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>chatroom</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<%- include('nav.ejs') %>
<!--
member[0] : 현재 로그인 아이디
member[1] : 글 작성자 아이디
-->
<section class="sec">
<div class="detail-bg">
<div class="title">
<h2><%= member[0].userid%>님과 <%= member[1].userid%>님의 채팅룸</h2>
</div>
<div class="chat-screen">
<% for( let i = 0; i < chat.length ; i++) {
let chatDate = chat[i].date.toLocaleDateString("ko-kr");
let prevDate;
if (i > 0) {
prevDate = chat[i-1].date.toLocaleDateString("ko-kr");
}
if (i == 0 || chatDate != prevDate) {
%>
<h3 class="chat-date"><%= chatDate %></h3>
<% } %>
<% if (chat[i].who.toString() == loginUserid) { %>
<div class="chat-box mine">
<% } else { %>
<div class="chat-box">
<% } %>
<span><%=chat[i].date.toLocaleTimeString("ko-kr", {hour : "2-digit", minute : "2-digit"}) %></span>
<span><%=chat[i].content %></span>
</div>
<% } %>
</div>
</div>
<div class="chat-form">
<input class="chat-input">
<button class="chat-button">전송</button>
</div>
</section>
<script src="https://cdn.jsdelivr.net/npm/socket.io@4.7.2/client-dist/socket.io.min.js"></script>
<script>
// socket.io-client 로드를 위한 snippet 추가
const socket = io();
socket.emit('ask-join', '<%= result._id %>');
document.querySelector('.chat-button').addEventListener('click', function () {
let msg = document.querySelector('.chat-input').value;
// main namespace에 event 방출 : 메시지 전송
socket.emit('message-send', {room: '<%= result._id %>', msg });
document.querySelector('.chat-input').value = null;
})
socket.on('message-broadcast', (data) => {
document.querySelector('.chat-screen')
.insertAdjacentHTML('beforeend',
`<div class="chat-box mine">
<span><%=new Date().toLocaleTimeString("ko-kr", {hour : "2-digit", minute : "2-digit"}) %></span>
<span>${data}</span>
</div>`
)
});
</script>
</body>
</html>
-
apple 사용자가 로그인 하고 채팅방에서 채팅을 보낼 때
-
tester 사용자가 로그인하고 채팅을 보낼 때