그룹채팅 - 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;

 

  1. 사용자가 채팅방에 입장했을 때
    - 사용자가 채팅방에 입장하면 해당 채팅방에 소켓을 추가합니다.
    - 사용자 입장 시스템 메시지를 생성하여 모든 사용자에게 전송합니다.
  2. 사용자가 채팅방을 나갈 때
    - 사용자가 채팅방을 나가면 해당 채팅방에서 소켓을 제거합니다.
    - 사용자 퇴장 시스템 메시지를 생성하여 모든 사용자에게 전송합니다.
소켓 제거
socket.leave(roomId);​

- 사용자의 소켓을 해당 채팅방에서 제거
- 사용자는 더 이상 그 채팅방에서 발생하는 메시지를 받지 않음