채팅방 스키마를 먼저 만들어 보자.
채팅방 생성->채팅방 접속->웹소켓 연결 및 구현->메세지 데이터 DB 연결 순서로 진행할 것이다.
Room Schema
- name
- paticipants
- userId
- userName
- userProfileImage
- joinedAt
- lastRead (안읽은 메세지 확인용. 마지막으로 읽은 시간)
- createdAt
- updatedAt
- lastMessage
1. 스키마 만들기
import mongoose from "mongoose";
const roomSchema = new mongoose.Schema({
name: {
type: String,
required: [true, "방 이름은 필수입니다."],
trim: true,
},
participants: [
{
userId: String,
username: String,
userProfileImage: String,
joinedAt: {
type: Date,
default: Date.now,
},
lastRead: {
type: Date,
default: Date.now,
},
},
],
lastMessage: {
content: String,
senderId: String,
senderName: String,
senderAvatar: String,
sentAt: Date,
},
createdAt: {
type: Date,
default: Date.now,
},
updatedAt: {
type: Date,
default: Date.now,
},
});
// 업데이트 시 updatedAt 자동 갱신
roomSchema.pre("save", function (next) {
this.updatedAt = new Date();
next();
});
export const Room = mongoose.model("Room", roomSchema);
생각해보니 user스키마도 없는데 room 스키마 부터 만들어놓고 participants를 넣는게 옳지 못하다는 생각이 드는데? 개발 먼저 해두고 추후에 로그인 인증 기능도 넣으려고 한다
어차피 혼자 진행하는 프로젝트니까... 개인이 생각했을 때 가장 효율을 높일 수 있는 순서로 👍👍
2. 채팅방 생성 API 만들기
routes 폴더를 생성해준 다음 room.js를 만들어 post api를 만들었다.
//routes/room.js
import express from "express";
import { Room } from "./models/Room.js";
const router = express.Router();
router.post("/rooms", async (req, res) => {
try {
const { name, userId, username, userProfileImage } = req.body;
// 필수 필드 검증
if (!name || !userId || !username) {
return res.status(400).json({
error: "방 이름과 사용자 정보는 필수입니다.",
});
}
// 채팅방 생성
const room = new Room({
name,
participants: [
{
userId,
username,
userProfileImage,
joinedAt: new Date(),
lastRead: new Date(),
},
],
// lastMessage는 처음에는 null로 시작
lastMessage: null,
});
// 데이터베이스에 저장
await room.save();
// 생성된 방 정보를 프론트엔드 형식으로 변환
const formattedRoom = {
id: room._id,
name: room.name,
participants: 1,
lastMessage: "",
lastActivityTime: "방금 전",
unreadCount: 0,
lastSender: null,
};
res.status(201).json({
message: "채팅방이 생성되었습니다.",
room: formattedRoom,
});
} catch (error) {
console.error("채팅방 생성 에러:", error);
res.status(500).json({
error: "채팅방 생성 중 오류가 발생했습니다.",
});
}
});
export default router;
3. Thunder Client로 테스트
Thunder Client란? Postman과 비슷하게 api를 테스트해볼 수 있는 프로그램이다.
VSCode에 설치하여 쉽게 쓸 수 있다.
개인적으로 postman은 느리고 무거워서 thunder client를 더 선호한다 ^^
위 사진처럼 좌측에 api 주소를 적고 아래 body에 폼을 채운 후 send를 누르면 우측에서 response를 확인할 수 있다.
Thunder Client Connection was forcibly closed by a peer. 에러
Connection was forcibly closed by a peer.
이런 메세지가 뜬다면 해당 포트가 사용중이라서 쓸 수 없는 것이다..
나는 다른 프로젝트를 3000번 포트로 하고 있는데, 터미널을 껐으므로 3000포트를 다시 사용해도 된다고 생각했다.
그런데 터미널을 꺼도 계속 돌아가고 있어서 이런 메세지가 계속 떴다.
포트 번호를 3001로 변경해서 해결했다.
netstat -ano | findstr :30
추가적으로 cmd에 명령어를 입력하면 어떤 프로세스가 3000번 포트를 쓰고 있는지 알 수 있다.
netstat -ano | findstr :3000
프로세스 ID가 4364인 프로세스가 3000번 포트를 쓰고 있다는 뜻이다.
taskkill /PID <프로세스ID> /F 로 프로세스를 종료할 수 있다.
프로세스에 대해 더 자세히 확인하려면 cmd에 아래 명령어를 입력하면 된다.
//window cmd
tasklist /FI "PID eq 4364"
또는 Git Bash에서 사용할 수 있는 대체 명령어:
//git bash
ps -W | grep 4364
Imgae Name: svchost.exe
PID: 4364
Session Name: Services
사용중인 메모리 양: 9876K
Session Name이 Services인 것은 시스템 프로세스다.
Session ID가 0인건 시스템 프로세스, 1인건 사용자 프로세스라고 한다.
4. Post API 결과 확인
mongoDB Compass에도 추가되었다.
5. 채팅방 불러오기 API 만들기
Post를 만들었으니 Get도 만들어보자.
//채팅방 불러오기 GET
router.get("/rooms", async (req, res) => {
try {
const rooms = await Room.find();
// 시간 차이를 계산하는 유틸리티 함수
const getTimeAgo = (date) => {
if (!date) return "";
const seconds = Math.floor((new Date() - date) / 1000);
if (seconds < 60) return "방금 전";
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}분 전`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}시간 전`;
const days = Math.floor(hours / 24);
return `${days}일 전`;
};
// 읽지 않은 메시지 수를 계산하는 함수
const getUnreadCount = async (room, userId) => {
const participant = room.participants.find((p) => p.userId === userId);
if (!participant) return 0;
// 사용자의 마지막 읽은 시간 이후의 메시지 수를 계산
const count = await Message.countDocuments({
roomId: room._id,
createdAt: { $gt: participant.lastRead },
});
return count;
};
// 프론트엔드에서 필요한 형식으로 데이터 변환
const formattedRooms = await Promise.all(
rooms.map(async (room) => ({
id: room._id,
name: room.name,
participants: room.participants.length,
// lastMessage 정보가 있으면 사용, 없으면 기본값 설정
lastMessage: room.lastMessage?.content || "",
lastActivityTime: getTimeAgo(
room.lastMessage?.sentAt || room.updatedAt
),
unreadCount: await getUnreadCount(room, req.query.userId), // userId는 쿼리 파라미터로 받음
lastSender: room.lastMessage
? {
name: room.lastMessage.senderName,
profileImage: room.lastMessage.senderProfileImage || "",
}
: null,
}))
);
res.json(formattedRooms);
} catch (error) {
console.error("채팅방 목록 조회 에러:", error);
res.status(500).json({ error: error.message });
}
});
getUnreadCount 함수
- 사용자가 읽지 않은 메시지 수를 계산한다. room.participants 배열에서 사용자를 찾아 해당 사용자의 lastRead 시간 이후에 작성된 메시지 수를 계산한다.
- Message.countDocuments를 사용하여 lastRead 이후에 작성된 메시지의 수를 조회한다.
Commit 에러
User@DESKTOP-T3DT9BF MINGW64 ~/Desktop/chat-app/chat-client (feat/#3/chatRoomListGet)
$ git commit -m "feat(postRoom)/#3: postRoom api 수정"
On branch feat/#3/chatRoomListGet
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: ../chat-server/models/Room.js
modified: ../chat-server/routes/rooms.js
no changes added to commit (use "git add" and/or "git commit -a")
잠깐… commit 하는데 또 오류가 난다
나는 chat-app이라는 폴더를 repository에 연결하고, 내부에 chat-client 폴더와 chat-server 폴더로 분리해 관리 중이다. chat-server 폴더의 스키마를 변경해놓고 chat-client 폴더에 들어가 커밋을 하려니 에러가 발생하는것..!
chat-server 폴더로 다시 들어가서 해결했다.
'프로젝트 > chat-app' 카테고리의 다른 글
react-router로 채팅방 라우팅하기 (0) | 2024.12.10 |
---|---|
채팅방 리스트 목록 구현하기 (0) | 2024.12.08 |
shadcn/ui 도입하기 (1) | 2024.12.06 |
채팅 앱 mongoDB 연결하기 (0) | 2024.12.05 |
react+socket.io로 채팅 앱 구현하기 (1) | 2024.12.04 |