그룹채팅 - 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를 확인하여 이미지를 적절히 처리하고 표시할 수 있습니다.