[RAPID] feat: 메신저 기능 구현 (Socket.IO 실시간 채팅)

- DB: messenger_rooms/participants/messages/reactions/files 테이블 생성
- Backend: REST API 9개 엔드포인트 + Socket.IO 실시간 핸들러
- Frontend: Gmail 스타일 FAB + 모달, 채팅방 목록, 채팅 패널
- 기능: DM/그룹/채널, 파일 첨부, 이모지 리액션, 멘션, 스레드
- 알림: 토스트 on/off 토글, FAB 읽지 않은 배지

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

[RAPID-fix] 메신저 API snake_case→camelCase 변환 및 Socket.IO URL 수정

- useRooms/useMessages/useCompanyUsers 훅에서 DB 응답 camelCase 변환
- Socket.IO 기본 연결 URL 3001 → 8080 수정
- runMigration.ts 마이그레이션 파일 경로 수정 (../../ → ../../../)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

[RAPID-fix] 방 생성 API camelCase/snake_case 호환 처리

- createRoom 컨트롤러에서 participantIds/type/name (camelCase) fallback 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

[RAPID-fix] 메시지 전송 API 추가 (sendMessage 라우트/컨트롤러 누락)

- POST /api/messenger/rooms/:roomId/messages 라우트 등록
- MessengerController.sendMessage 메서드 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-30 18:05:54 +09:00
parent e763249342
commit f558073ef8
28 changed files with 2578 additions and 10 deletions

View File

@@ -0,0 +1,202 @@
import { Request, Response } from 'express';
import { messengerService } from '../services/messengerService';
import { AuthenticatedRequest } from '../types/auth';
import path from 'path';
class MessengerController {
async getRooms(req: Request, res: Response) {
try {
const user = (req as AuthenticatedRequest).user!;
const rooms = await messengerService.getRooms(user.userId, user.companyCode!);
res.json({ success: true, data: rooms });
} catch (error) {
const err = error as Error;
console.error('getRooms error:', err.message);
res.status(500).json({ success: false, message: err.message });
}
}
async createRoom(req: Request, res: Response) {
try {
const user = (req as AuthenticatedRequest).user!;
const room_type = req.body.room_type ?? req.body.type;
const room_name = req.body.room_name ?? req.body.name;
const participant_ids = req.body.participant_ids ?? req.body.participantIds;
if (!room_type || !participant_ids || !Array.isArray(participant_ids)) {
return res.status(400).json({ success: false, message: 'room_type and participant_ids are required.' });
}
const room = await messengerService.createRoom(user.userId, user.companyCode!, {
room_type,
room_name,
participant_ids,
});
res.json({ success: true, data: room });
} catch (error) {
const err = error as Error;
console.error('createRoom error:', err.message);
res.status(500).json({ success: false, message: err.message });
}
}
async getMessages(req: Request, res: Response) {
try {
const user = (req as AuthenticatedRequest).user!;
const roomId = parseInt(req.params.roomId, 10);
const limit = parseInt(req.query.limit as string, 10) || 50;
const before = req.query.before ? parseInt(req.query.before as string, 10) : undefined;
const messages = await messengerService.getMessages(roomId, user.userId, user.companyCode!, limit, before);
res.json({ success: true, data: messages });
} catch (error) {
const err = error as Error;
console.error('getMessages error:', err.message);
res.status(500).json({ success: false, message: err.message });
}
}
async sendMessage(req: Request, res: Response) {
try {
const user = (req as AuthenticatedRequest).user!;
const roomId = parseInt(req.params.roomId, 10);
const content = req.body.content;
const messageType = req.body.type ?? req.body.message_type ?? 'text';
const parentId = req.body.parentId ?? req.body.parent_message_id ?? null;
if (!content) {
return res.status(400).json({ success: false, message: 'content is required.' });
}
const message = await messengerService.sendMessage(roomId, user.userId, user.companyCode!, content, messageType, parentId);
res.json({ success: true, data: message });
} catch (error) {
const err = error as Error;
console.error('sendMessage error:', err.message);
res.status(500).json({ success: false, message: err.message });
}
}
async markAsRead(req: Request, res: Response) {
try {
const user = (req as AuthenticatedRequest).user!;
const roomId = parseInt(req.params.roomId, 10);
await messengerService.markAsRead(roomId, user.userId);
res.json({ success: true });
} catch (error) {
const err = error as Error;
console.error('markAsRead error:', err.message);
res.status(500).json({ success: false, message: err.message });
}
}
async uploadFile(req: Request, res: Response) {
try {
const user = (req as AuthenticatedRequest).user!;
const files = req.files as Express.Multer.File[];
if (!files || files.length === 0) {
return res.status(400).json({ success: false, message: 'No files uploaded.' });
}
const roomId = parseInt(req.body.room_id, 10);
if (!roomId) {
return res.status(400).json({ success: false, message: 'room_id is required.' });
}
const savedFiles = [];
for (const file of files) {
// Create a file message
const message = await messengerService.sendMessage(
roomId,
user.userId,
user.companyCode!,
file.originalname,
'file'
);
const savedFile = await messengerService.saveFile(message.id, {
originalName: file.originalname,
storedName: file.filename,
filePath: file.path,
fileSize: file.size,
mimeType: file.mimetype,
});
savedFiles.push({ message, file: savedFile });
}
res.json({ success: true, data: savedFiles });
} catch (error) {
const err = error as Error;
console.error('uploadFile error:', err.message);
res.status(500).json({ success: false, message: err.message });
}
}
async downloadFile(req: Request, res: Response) {
try {
const fileId = parseInt(req.params.fileId, 10);
const file = await messengerService.getFileById(fileId);
if (!file) {
return res.status(404).json({ success: false, message: 'File not found.' });
}
res.download(file.file_path, file.original_name);
} catch (error) {
const err = error as Error;
console.error('downloadFile error:', err.message);
res.status(500).json({ success: false, message: err.message });
}
}
async getCompanyUsers(req: Request, res: Response) {
try {
const user = (req as AuthenticatedRequest).user!;
const users = await messengerService.getCompanyUsers(user.companyCode!, user.userId);
res.json({ success: true, data: users });
} catch (error) {
const err = error as Error;
console.error('getCompanyUsers error:', err.message);
res.status(500).json({ success: false, message: err.message });
}
}
async updateRoom(req: Request, res: Response) {
try {
const user = (req as AuthenticatedRequest).user!;
const roomId = parseInt(req.params.roomId, 10);
const { room_name } = req.body;
if (!room_name) {
return res.status(400).json({ success: false, message: 'room_name is required.' });
}
const room = await messengerService.updateRoom(roomId, user.companyCode!, room_name);
if (!room) {
return res.status(404).json({ success: false, message: 'Room not found.' });
}
res.json({ success: true, data: room });
} catch (error) {
const err = error as Error;
console.error('updateRoom error:', err.message);
res.status(500).json({ success: false, message: err.message });
}
}
async getUnreadCount(req: Request, res: Response) {
try {
const user = (req as AuthenticatedRequest).user!;
const count = await messengerService.getUnreadCount(user.userId, user.companyCode!);
res.json({ success: true, data: { unread_count: count } });
} catch (error) {
const err = error as Error;
console.error('getUnreadCount error:', err.message);
res.status(500).json({ success: false, message: err.message });
}
}
}
export const messengerController = new MessengerController();