그룹채팅 - Part5 채팅방 시스템 메시지
ㆍProject Diary/Next.js + Prisma + MariaDB (KiloFlow)
이번 포스팅에서는 채팅방에서 시스템 메시지를 구현하는 방법에 대해 알아보겠습니다. 시스템 메시지는 사용자가 채팅방에 입장하거나 퇴장할 때, 또는 기타 중요한 이벤트가 발생했을 때 사용자들에게 자동으로 전달되는 메시지입니다. 이를 통해 채팅방의 상태를 사용자들에게 알릴 수 있습니다.
데이터베이스 스키마
chatMessages 테이블
model chatMessages {
id Int @id @default(autoincrement())
chatroom_id Int
user_id Int?
message String?
image_id Int?
created_at DateTime @default(now())
chatroom chatrooms @relation(fields: [chatroom_id], references: [id], onDelete: Cascade)
user users? @relation(fields: [user_id], references: [user_id], onDelete: Cascade)
image chatImageMessage? @relation(fields: [image_id], references: [id])
@@index([chatroom_id])
@@index([user_id])
@@index([image_id])
}
시스템 메시지를 저장하기 위해 기존의 chatMessages 모델을 그대로 사용하되, user_id 필드를 null로 설정하여 시스템 메시지임을 구분합니다.
프론트엔드
메시지 렌더링 컴포넌트
import React, { useEffect, useState } from "react";
import Image from "next/image";
import styled from "styled-components";
import unknownUser from "../../public/unknownUser.jpg";
// 스타일 정의
const MessageContainer = styled.div<{
isCurrentUser: boolean;
isSystemMessage?: boolean;
}>`
display: flex;
flex-direction: ${({ isCurrentUser }) =>
isCurrentUser ? "row-reverse" : "row"};
align-items: flex-end;
margin-bottom: 10px;
justify-content: ${({ isSystemMessage }) =>
isSystemMessage ? "center" : "flex-start"};
.message__content {
max-width: 60%;
background-color: ${({ isCurrentUser, isSystemMessage }) =>
isSystemMessage ? "transparent" : isCurrentUser ? "#dcf8c6" : "#fff"};
border-radius: 10px;
padding: 10px;
margin-left: ${({ isCurrentUser }) => (isCurrentUser ? "0" : "5px")};
margin-right: ${({ isCurrentUser }) => (isCurrentUser ? "5px" : "0")};
word-break: break-word;
text-align: ${({ isSystemMessage }) =>
isSystemMessage ? "center" : "left"};
font-weight: ${({ isSystemMessage }) =>
isSystemMessage ? "bold" : "normal"};
position: relative;
}
`;
// 메시지 타입 정의
interface Message {
id: number;
user_id: number | null;
message: string | null;
created_at: string;
image_id: number | null;
}
// 사용자 타입 정의
interface User {
user_id: number;
nickname: string;
profile_image: string;
}
// 컴포넌트 프로퍼티 정의
interface ChatMessageProps {
message: Message;
isCurrentUser: boolean;
isSystemMessage: boolean;
messageUser?: User;
formatTime: (time: string) => string;
}
const ChatMessage: React.FC<ChatMessageProps> = ({
message,
isCurrentUser,
isSystemMessage,
messageUser,
formatTime,
}) => {
const [imagePath, setImagePath] = useState<string | null>(null);
// 메시지에 이미지가 포함된 경우 이미지 경로를 가져옴
useEffect(() => {
if (message.image_id) {
fetch(`/api/community/upload?id=${message.image_id}`)
.then((res) => res.json())
.then((data) => {
if (data.image) {
setImagePath(data.image.path);
}
})
.catch((error) => {
console.error("Error fetching image:", error);
});
}
}, [message.image_id]);
return (
<MessageContainer isCurrentUser={isCurrentUser} isSystemMessage={isSystemMessage}>
{/* 시스템 메시지 또는 본인 메시지가 아닌 경우에만 프로필 이미지를 표시 */}
{!isCurrentUser && !isSystemMessage && (
<Image
src={messageUser ? messageUser.profile_image : unknownUser}
alt="프로필"
className="profile__image"
width={40}
height={40}
/>
)}
<div className="message__content">
{/* 시스템 메시지 또는 본인 메시지가 아닌 경우에만 닉네임을 표시 */}
{!isCurrentUser && !isSystemMessage && (
<div className="nickname">
{messageUser ? messageUser.nickname : "알 수 없는 사용자"}
</div>
)}
{/* 메시지 내용을 표시 */}
{message.message && <div>{message.message}</div>}
{/* 이미지가 있는 경우 이미지를 표시 */}
{imagePath && (
<div className="image__content">
<Image
src={imagePath}
alt="Uploaded file"
width={100}
height={100}
/>
</div>
)}
{/* 시스템 메시지가 아닌 경우 메시지 시간을 표시 */}
{!isSystemMessage && (
<div className="message__time">{formatTime(message.created_at)}</div>
)}
</div>
</MessageContainer>
);
};
export default ChatMessage;
- isSystemMessage prop을 통해 시스템 메시지를 구분하고 스타일을 다르게 적용합니다.
- 시스템 메시지 또는 본인 메시지가 아닌 경우에만 프로필 이미지와 닉네임을 표시합니다.
백엔드
시스템 메시지 생성 및 전송
소켓 서버
api/socket.ts
const SocketHandler = (req: NextApiRequest, res: NextApiResponseWithSocket) => {
if (!res.socket.server.io) {
console.log("Initializing socket.io server...");
const io = new Server(res.socket.server as SocketServer, {
path: "/api/community/socket",
});
res.socket.server.io = io;
io.on("connection", (socket) => {
console.log("New socket connection");
// 사용자가 채팅방에 입장할 때 실행
socket.on("join_room", async ({ roomId, userId }) => {
try {
console.log(`User ${userId} joined room ${roomId}`);
socket.join(roomId);
// 시스템 메시지 생성: 사용자 입장 알림
const user = await prisma.users.findUnique({
where: { user_id: Number(userId) },
});
if (user) {
const systemMessage = await prisma.chatMessages.create({
data: {
chatroom_id: Number(roomId),
user_id: null, // 시스템 메시지는 user_id가 null
message: `${user.nickname}님이 입장했습니다.`, // 메시지 내용
created_at: new Date(), // 메시지 생성 시간
},
});
io.to(roomId).emit("new_message", systemMessage);
}
} catch (error) {
console.error("Error in join_room:", error);
}
});
// 사용자가 채팅방을 나갈 때 실행
socket.on("leave_room", async ({ roomId, userId }) => {
try {
console.log(`User ${userId} left room ${roomId}`);
socket.leave(roomId); // 소켓제거
// 시스템 메시지 생성: 사용자 퇴장 알림
const user = await prisma.users.findUnique({
where: { user_id: Number(userId) },
});
if (user) {
const systemMessage = await prisma.chatMessages.create({
data: {
chatroom_id: Number(roomId),
user_id: null, // 시스템 메시지는 user_id가 null
message: `${user.nickname}님이 나갔습니다.`, // 메시지 내용
created_at: new Date(), // 메시지 생성 시간
},
});
io.to(roomId).emit("new_message", systemMessage);
}
} catch (error) {
console.error("Error in leave_room:", error);
}
});
socket.on("disconnect", () => {
console.log("Socket disconnected:", socket.id);
});
});
}
res.end();
};
export default SocketHandler;
- 사용자가 채팅방에 입장했을 때
- 사용자가 채팅방에 입장하면 해당 채팅방에 소켓을 추가합니다.
- 사용자 입장 시스템 메시지를 생성하여 모든 사용자에게 전송합니다. - 사용자가 채팅방을 나갈 때
- 사용자가 채팅방을 나가면 해당 채팅방에서 소켓을 제거합니다.
- 사용자 퇴장 시스템 메시지를 생성하여 모든 사용자에게 전송합니다.
소켓 제거
socket.leave(roomId);
- 사용자의 소켓을 해당 채팅방에서 제거
- 사용자는 더 이상 그 채팅방에서 발생하는 메시지를 받지 않음