그룹채팅 - Part4 사진 전송

Project Diary/Next.js + Prisma + MariaDB (KiloFlow)

사용자가 채팅방에서 사진을 전송하는 기능을 구현하는 방법에 대해 설명하겠습니다. 


 

데이터베이스 스키마

 

chatImageMessage 테이블

model chatImageMessage {
  id          Int            @id @default(autoincrement())
  path        String
  createdAt   DateTime       @default(now())
  chatMessages chatMessages[]

  @@index([id])
}

이미지 파일의 경로를 저장합니다.

 

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])
}
  • image_id 필드를 추가하여 이미지 메시지를 저장할 수 있습니다.
  • image 필드
    - mage_id 필드는 chatMessages 테이블에서 chatImageMessage 테이블의 id 필드를 참조합니다.

 

프론트엔드

 

1. 파일 선택 및 상태 관리

const [selectedFile, setSelectedFile] = useState<File | null>(null); // 선택된 파일 상태를 관리합니다.

 

2. 파일 선택 시 실행되는 함수

const handleFileChange = async (event: ChangeEvent<HTMLInputElement>) => {
  if (event.target.files && event.target.files[0]) {
    setSelectedFile(event.target.files[0]); // 선택된 파일 상태를 업데이트합니다.

    const formData = new FormData();
    formData.append("file", event.target.files[0]); // 선택된 파일을 폼 데이터에 추가합니다.

    const res = await fetch("/api/community/upload", {
      method: "POST",
      body: formData, // 폼 데이터를 요청 본문에 포함하여 서버에 전송합니다.
    });

    if (res.ok) {
      const data = await res.json();
      sendMessageWithFile(data.file.id); // 파일 ID를 포함한 메시지 전송 함수 호출
      setSelectedFile(null); // 선택된 파일 상태를 초기화합니다.
    }
  }
};

 

3. 파일 ID를 포함한 메시지를 서버로 전송하는 함수

const sendMessageWithFile = (fileId: number) => {
  socket.emit("send_message", {
    roomId: "ROOM_ID", // 현재 채팅방 ID (실제로는 적절한 값을 넣어야 합니다)
    image_id: fileId, // 파일 ID
  });
};

 

4. 메시지 렌더링 컴포넌트

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]);

{imagePath && (
  <div className="image__content">
    <Image
      src={imagePath}
      alt="Uploaded file"
      width={100}
      height={100}
    />
  </div>
)}
  • 이미지 경로를 상태로 관리하고, 이미지 ID가 있으면 서버에서 이미지를 가져와 상태를 업데이트합니다.
  • agePath && ( <div className="image__content"> <Image src={imagePath} alt="Uploaded file" width={100} height={100} /> </div> )}
    이미지가 있는 경우 이미지를 표시합니다.

 


 

백엔드

 

1. 이미지 업로드 핸들러

 

1-1 Next.js API 설정

export const config = {
  api: {
    bodyParser: false, // Next.js의 기본 bodyParser를 비활성화합니다.
  },
};
  • Multer를 사용하려면 bodyParser를 비활성화해야 합니다.

1-2 미들웨어 실행 함수

const runMiddleware = (
  req: NextApiRequest,
  res: NextApiResponse,
  fn: Function
) => {
  return new Promise((resolve, reject) => {
    fn(req, res, (result: any) => {
      if (result instanceof Error) {
        return reject(result);
      }
      return resolve(result);
    });
  });
};

 

1-3 이미지 업로드 핸들러 함수

const uploadHandler = async (req: NextApiRequest, res: NextApiResponse) => {
  await runMiddleware(req, res, upload.single("file")); // Multer 미들웨어를 실행하여 파일을 처리합니다.

  const extendedReq = req as ExtendedRequest;
  const { file } = extendedReq;

  if (!file) {
    return res.status(400).json({ error: "File is required" }); // 파일이 없으면 오류 응답을 반환합니다.
  }

  const path = `/uploads/${file.filename}`; // 파일 경로를 설정합니다.

  try {
    const savedFile = await prisma.chatImageMessage.create({
      data: {
        path: path, // 파일 경로를 데이터베이스에 저장합니다.
      },
    });

    return res.status(200).json({ file: savedFile }); // 저장된 파일 정보를 클라이언트에 반환합니다.
  } catch (error) {
    console.error(error);
    return res.status(500).json({ error: "Error saving file" }); // 오류 발생 시 오류 응답을 반환합니다.
  }
};

 

1-4  이미지 가져오기 핸들러 함수

const getImageHandler = async (req: NextApiRequest, res: NextApiResponse) => {
  const { id } = req.query;

  try {
    const image = await prisma.chatImageMessage.findUnique({
      where: { id: Number(id) },
    });

    if (image) {
      return res.status(200).json({ image }); // 이미지를 찾으면 응답합니다.
    } else {
      return res.status(404).json({ error: "Image not found" }); // 이미지를 찾지 못하면 오류 응답을 반환합니다.
    }
  } catch (error) {
    console.error(error);
    return res.status(500).json({ error: "Error fetching image" }); // 오류 발생 시 오류 응답을 반환합니다.
  }
};

주어진 ID를 사용하여 데이터베이스에서 이미지를 조회하고, 이미지를 클라이언트에 반환합니다.

 

1-5 API 라우트 핸들러 함수

export default (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method === "POST") {
    return uploadHandler(req, res); // POST 요청이면 업로드 핸들러를 실행합니다.
  } else if (req.method === "GET") {
    return getImageHandler(req, res); // GET 요청이면 이미지 가져오기 핸들러를 실행합니다.
  } else {
    res.setHeader("Allow", ["POST", "GET"]); // 허용되지 않은 메서드에 대한 응답을 설정합니다.
    res.status(405).end(`Method ${req.method} Not Allowed`); // 허용되지 않은 메서드에 대한 오류 응답을 반환합니다.
  }
};

POST 요청은 uploadHandler를 실행하고, GET 요청은 getImageHandler를 실행합니다.

 

 

2. 소켓 서버 설정

 

사용자가 메시지를 전송할 때

socket.on("send_message", async ({ roomId, userId, message, image_id }) => {
  const newMessage = await prisma.chatMessages.create({
    data: {
      chatroom_id: Number(roomId),
      user_id: Number(userId),
      message,
      image_id, // 추가
      created_at: new Date(),
    },
  });
  io.to(roomId).emit("new_message", newMessage); // 채팅방의 모든 사용자에게 새 메시지를 전송합니다.
});
  • 사용자가 메시지를 전송할 때 이미지가 포함된 경우, image_id를 데이터베이스에 저장하고 클라이언트에 전송하기 위함입니다.
  • 이렇게 하면 클라이언트는 수신한 메시지에서 image_id를 확인하여 이미지를 적절히 처리하고 표시할 수 있습니다.