diff --git a/app.js b/app.js index dcc56c03adc0476f9a365a184f6c16e66829a4d7..f343703673275bd6a1a1e53a27d3055cdd2326e9 100644 --- a/app.js +++ b/app.js @@ -1,4 +1,3 @@ - // app.js require('dotenv').config(); @@ -7,10 +6,23 @@ const express = require('express'); const session = require('express-session'); const passport = require('./passport'); // 변경된 경로 const flash = require('connect-flash'); -const { initScheduleCleaner } = require('./utils/scheduler'); // 유동 스케줄 자동 삭제 유틸 +const { initScheduleCleaner } = require('./utils/scheduler'); +const connectMongoDB = require('./config/mongoose'); // MongoDB 연결 +const { sequelize } = require('./config/sequelize'); // Sequelize 연결 +const cors = require('cors'); const app = express(); +// CORS 설정 +app.use( + cors({ + origin: 'http://localhost:3000', // 허용할 도메인 설정 (예: 프론트엔드 주소) + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization'], + credentials: true, // 쿠키와 인증 정보를 허용하려면 true로 설정 + }) +); + // 미들웨어 설정 app.use(express.json()); @@ -35,28 +47,43 @@ app.use(flash()); /** * 라우터 등록 */ -// 로그인 라우터 const authRoutes = require('./routes/auth'); app.use('/auth', authRoutes); -// Schedule 라우터 const scheduleRoutes = require('./routes/schedule'); app.use('/api/schedule', scheduleRoutes); -// Friend 라우터 const friendRoutes = require('./routes/friend'); app.use('/api/friend', friendRoutes); +const meetingRoutes = require('./routes/meetingRoute'); +app.use('/api/meeting', meetingRoutes); +const chatRoutes = require('./routes/chatRoute'); +app.use('/api/chat', chatRoutes); +// 스케줄 클리너 초기화 initScheduleCleaner(); const PORT = process.env.PORT || 3000; -app.get('/', (req, res) => { - res.send('Hello, World!'); -}); - -app.listen(PORT, () => { - console.log(`Server is running on http://localhost:${PORT}`); -}); +// MongoDB 및 MySQL 연결 후 서버 시작 +(async () => { + try { + // MongoDB 연결 + await connectMongoDB(); + console.log('✅ MongoDB 연결 성공'); + + // MySQL 연결 확인 + await sequelize.authenticate(); + console.log('✅ MySQL 연결 성공'); + + // 서버 시작 + app.listen(PORT, () => { + console.log(`Server is running on http://localhost:${PORT}`); + }); + } catch (error) { + console.error('❌ 서버 시작 중 오류 발생:', error); + process.exit(1); + } +})(); \ No newline at end of file diff --git a/config/mongoose.js b/config/mongoose.js index 3379779bf664ebd969317df7f7de48e7a74b34be..692a4aa2a9d31e14916b06892469db5e81ad980e 100644 --- a/config/mongoose.js +++ b/config/mongoose.js @@ -2,17 +2,17 @@ const mongoose = require('mongoose'); -mongoose.connect(process.env.MONGO_URI, { - useNewUrlParser: true, - useUnifiedTopology: true, -}); +const connectMongoDB = async () => { + try { + await mongoose.connect(process.env.MONGODB_URI, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); + console.log('✅ MongoDB 연결 성공'); + } catch (error) { + console.error('❌ MongoDB 연결 실패:', error); + throw error; + } +}; -mongoose.connection.on('connected', () => { - console.log('Mongoose connected.'); -}); - -mongoose.connection.on('error', (err) => { - console.error('Mongoose connection error:', err); -}); - -module.exports = mongoose; +module.exports = connectMongoDB; \ No newline at end of file diff --git a/controllers/chatController.js b/controllers/chatController.js index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..4c61e515f0748955dc020a4e21f4441ffcf7ffe5 100644 --- a/controllers/chatController.js +++ b/controllers/chatController.js @@ -0,0 +1,105 @@ +const chatService = require('../services/chatService'); + +// 내부용 채팅방 생성 +exports.createChatRoomInternal = async (params) => { + try { + return await chatService.createChatRoom(params); + } catch (err) { + console.error('Error in createChatRoomInternal:', err); + return { success: false, error: err.message }; + } +}; + +// 새 채팅방 생성 +exports.createChatRoom = async (req, res) => { + try { + const chatRoomId = await chatService.createChatRoom(); + res.json({ chatRoomId }); + } catch (err) { + console.error('Error creating room:', err); + res.status(500).json({ error: 'Failed to create room' }); + } +}; + +// 채팅방 목록 조회 +exports.getChatRooms = async (req, res) => { + try { + const roomData = await chatService.getChatRooms(); + res.json(roomData); + } catch (err) { + console.error('Error fetching rooms:', err); + res.status(500).json({ error: 'Failed to fetch rooms' }); + } +}; + +// 사용자 상태 업데이트 +exports.updateStatus = async (req, res) => { + const { chatRoomId, nickname, isOnline } = req.body; + try { + await chatService.updateStatus(chatRoomId, nickname, isOnline); + res.status(200).json({ message: 'User status updated successfully' }); + } catch (err) { + console.error('Error updating user status:', err); + res.status(500).json({ error: 'Failed to update user status' }); + } +}; + +// 읽음 상태 업데이트 +exports.updateReadStatus = async (req, res) => { + const { chatRoomId, nickname } = req.body; + try { + await chatService.updateReadStatus(chatRoomId, nickname); + res.status(200).json({ message: 'Read status updated' }); + } catch (err) { + console.error('Error updating read status:', err); + res.status(500).json({ error: 'Failed to update read status' }); + } +}; + +// 읽지 않은 메시지 조회 +exports.getUnreadMessages = async (req, res) => { + const { nickname } = req.params; + try { + const unreadMessages = await chatService.getUnreadMessages(nickname); + res.status(200).json(unreadMessages); + } catch (err) { + console.error('Error fetching unread messages:', err); + res.status(500).json({ error: 'Failed to fetch unread messages' }); + } +}; + +// 읽지 않은 메시지 수 조회 +exports.getUnreadCount = async (req, res) => { + const { chatRoomId } = req.params; + try { + const unreadCountMap = await chatService.getUnreadCount(chatRoomId); + res.status(200).json(unreadCountMap); + } catch (err) { + console.error('Error fetching unread counts:', err); + res.status(500).json({ error: 'Failed to fetch unread counts' }); + } +}; + +// 읽은 로그 ID 업데이트 +exports.updateReadLogId = async (req, res) => { + const { chatRoomId, nickname, logId } = req.body; + try { + await chatService.updateReadLogId(chatRoomId, nickname, logId); + res.status(200).json({ message: 'Last read logID updated' }); + } catch (err) { + console.error('Error updating last read logID:', err); + res.status(500).json({ error: 'Failed to update last read logID' }); + } +}; + +// 상태와 로그 ID 동시 업데이트 +exports.updateStatusAndLogId = async (req, res) => { + const { chatRoomId, nickname, isOnline, logId } = req.body; + try { + await chatService.updateStatusAndLogId(chatRoomId, nickname, isOnline, logId); + res.status(200).json({ message: 'User status and lastReadLogId updated successfully' }); + } catch (err) { + console.error('Error updating user status and lastReadLogId:', err); + res.status(500).json({ error: 'Failed to update user status and lastReadLogId' }); + } +}; \ No newline at end of file diff --git a/controllers/meetingController.js b/controllers/meetingController.js index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..4860322910afe398d397dafe2a38044979d42930 100644 --- a/controllers/meetingController.js +++ b/controllers/meetingController.js @@ -0,0 +1,68 @@ +const MeetingService = require('../services/meetingService'); + +class MeetingController { + async createMeeting(req, res) { + try { + const result = await MeetingService.createMeeting(req.body); + res.status(201).json(result); + } catch (err) { + console.error('번개 모임 생성 오류:', err); + res.status(500).json({ error: err.message || '번개 모임 생성 실패' }); + } + } + + async getMeetings(req, res) { + const { userId } = req.query; + + if (!userId) { + return res.status(400).json({ error: '사용자 ID가 필요합니다.' }); + } + + try { + const meetings = await MeetingService.getMeetings(userId); + res.status(200).json(meetings); + } catch (err) { + console.error('모임 목록 조회 오류:', err); + res.status(500).json({ error: err.message || '모임 목록 조회 실패' }); + } + } + + async closeMeeting(req, res) { + const { meetingId } = req.params; + + try { + const meeting = await MeetingService.closeMeeting(meetingId); + res.status(200).json({ message: '모임이 마감되었습니다.', meeting }); + } catch (err) { + console.error('모임 마감 오류:', err); + res.status(500).json({ error: err.message || '모임 마감 실패' }); + } + } + + async joinMeeting(req, res) { + const { meetingId } = req.params; + const { user_id } = req.body; + + try { + await MeetingService.joinMeeting(meetingId, user_id); + res.status(200).json({ message: '모임 및 채팅방 참가 완료' }); + } catch (err) { + console.error('모임 참가 오류:', err); + res.status(500).json({ error: err.message || '모임 참가 실패' }); + } + } + + async getMeetingDetail(req, res) { + const { meetingId } = req.params; + + try { + const meetingDetail = await MeetingService.getMeetingDetail(meetingId); + res.status(200).json(meetingDetail); + } catch (err) { + console.error('모임 상세 조회 오류:', err); + res.status(500).json({ error: err.message || '모임 상세 조회 실패' }); + } + } +} + +module.exports = new MeetingController(); \ No newline at end of file diff --git a/dtos/MeetingDetailResponse.js b/dtos/MeetingDetailResponse.js new file mode 100644 index 0000000000000000000000000000000000000000..6670fe49a85573d4654b2e13c5b31a28572fbac0 --- /dev/null +++ b/dtos/MeetingDetailResponse.js @@ -0,0 +1,20 @@ +class MeetingDetailResponse { + constructor(meeting) { + this.id = meeting.id; + this.title = meeting.title; + this.description = meeting.description; + this.startTime = meeting.start_time; + this.endTime = meeting.end_time; + this.location = meeting.location; + this.deadline = meeting.deadline; + this.type = meeting.type; + this.creatorName = meeting.creator ? meeting.creator.name : 'Unknown'; + this.participants = meeting.participants.map(participant => ({ + userId: participant.user_id, + name: participant.participantUser ? participant.participantUser.name : 'Unknown', + email: participant.participantUser ? participant.participantUser.email : 'Unknown' + })); + } +} + +module.exports = MeetingDetailResponse; \ No newline at end of file diff --git a/dtos/MeetingResponse.js b/dtos/MeetingResponse.js new file mode 100644 index 0000000000000000000000000000000000000000..8ee8b833c23b8ff6fb70d200f4384965230f57b5 --- /dev/null +++ b/dtos/MeetingResponse.js @@ -0,0 +1,19 @@ +// dtos/MeetingResponse.js + +class MeetingResponse { + constructor(meeting, isParticipant, isScheduleConflict, creatorName) { + this.id = meeting.id; + this.title = meeting.title; + this.description = meeting.description; + this.startTime = meeting.start_time; + this.endTime = meeting.end_time; + this.location = meeting.location; + this.deadline = meeting.deadline; + this.type = meeting.type; + this.creatorName = creatorName; + this.isParticipant = isParticipant; + this.isScheduleConflict = isScheduleConflict; + } +} + +module.exports = MeetingResponse; \ No newline at end of file diff --git a/models/chatRooms.js b/models/chatRooms.js new file mode 100644 index 0000000000000000000000000000000000000000..ca68c3941a49d64f8abd4806c399793c03590301 --- /dev/null +++ b/models/chatRooms.js @@ -0,0 +1,21 @@ +const mongoose = require('mongoose'); + +// MongoDB 채팅방 스키마 수정 (현재 참가 중인 유저 목록 추가) +const chatRoomsSchema = new mongoose.Schema({ + chatRoomId: { type: String, required: true, unique: true }, + messages: [{ + sender: String, + message: String, + timestamp: Date, + type: { type: String, default: 'message' } // 기본값은 'message', 다른 값으로 'join', 'leave' 가능 + }], + participants: [{ type: String }], + lastReadAt: { type: Map, of: Date }, // 각 참가자의 마지막 읽은 메시지 시간 기록 + lastReadLogId: { type: Map, of: String }, // 각 참가자의 마지막으로 읽은 logID 기록 + isOnline: { type: Map, of: Boolean } // 각 참가자의 온라인 상태 +}, { collection: 'chatrooms' }); + +// 모델이 이미 정의되어 있는 경우 재정의하지 않음 +const ChatRooms = mongoose.models.ChatRooms || mongoose.model('ChatRooms', chatRoomsSchema); + +module.exports = ChatRooms; \ No newline at end of file diff --git a/models/meeting.js b/models/meeting.js index 7944e355fef21458c8550ce46bbb6eeb7e3cbc46..14c50b58aa78c52bd14340c678778a49a8d1c994 100644 --- a/models/meeting.js +++ b/models/meeting.js @@ -1,10 +1,15 @@ // models/Meeting.js const { DataTypes } = require('sequelize'); -const sequelize = require('../config/sequelize'); +const { sequelize } = require('../config/sequelize'); const User = require('./User'); const Meeting = sequelize.define('Meeting', { + id: { + type: DataTypes.BIGINT, + primaryKey: true, + autoIncrement: true, + }, title: { type: DataTypes.STRING, allowNull: false, @@ -30,12 +35,34 @@ const Meeting = sequelize.define('Meeting', { type: DataTypes.ENUM('OPEN', 'CLOSE'), allowNull: false, }, + created_by: { + type: DataTypes.BIGINT, + allowNull: false, + references: { + model: 'Users', + key: 'id', + }, + }, + chatRoomId: { // 새로운 필드 추가 + type: DataTypes.STRING, + allowNull: false, + }, }, { tableName: 'Meetings', timestamps: false, }); -Meeting.belongsTo(User, { foreignKey: 'created_by', as: 'creator' }); -User.hasMany(Meeting, { foreignKey: 'created_by', as: 'meetings' }); -module.exports = Meeting; +// 연관 관계 설정 +Meeting.associate = (models) => { + Meeting.belongsTo(models.User, { + foreignKey: 'created_by', // FK 설정 + as: 'creator', // 별칭 + }); + Meeting.hasMany(models.MeetingParticipant, { + foreignKey: 'meeting_id', + as: 'participants', + }); +}; + +module.exports = Meeting; \ No newline at end of file diff --git a/models/meetingParticipant.js b/models/meetingParticipant.js index 716a4420b9c9205398cd8d426b0e7ef4c349fad0..15c93c13aa7e6721fb250422333bcd772ffc0f1c 100644 --- a/models/meetingParticipant.js +++ b/models/meetingParticipant.js @@ -1,19 +1,32 @@ // models/MeetingParticipant.js const { DataTypes } = require('sequelize'); -const sequelize = require('../config/sequelize'); -const Meeting = require('./Meeting'); -const User = require('./User'); +const { sequelize } = require('../config/sequelize'); -const MeetingParticipant = sequelize.define('MeetingParticipant', {}, { +const MeetingParticipant = sequelize.define('MeetingParticipant', { + meeting_id: { + type: DataTypes.BIGINT, + allowNull: false + }, + user_id: { + type: DataTypes.BIGINT, + allowNull: false + } +}, { tableName: 'MeetingParticipants', timestamps: false, }); -MeetingParticipant.belongsTo(Meeting, { foreignKey: 'meeting_id', as: 'meeting' }); -Meeting.hasMany(MeetingParticipant, { foreignKey: 'meeting_id', as: 'participants' }); +MeetingParticipant.associate = (models) => { + MeetingParticipant.belongsTo(models.Meeting, { + foreignKey: 'meeting_id', + as: 'meeting' + }); -MeetingParticipant.belongsTo(User, { foreignKey: 'user_id', as: 'user' }); -User.hasMany(MeetingParticipant, { foreignKey: 'user_id', as: 'meetingParticipations' }); + MeetingParticipant.belongsTo(models.User, { + foreignKey: 'user_id', + as: 'participantUser' + }); +}; module.exports = MeetingParticipant; diff --git a/models/user.js b/models/user.js index fbf92c144d9a6de814321390a784bc4c66c5d2eb..cb14b0bc547c79ac88535d4d88c4a4331e6d113a 100644 --- a/models/user.js +++ b/models/user.js @@ -1,9 +1,14 @@ // models/User.js const { DataTypes } = require('sequelize'); -const sequelize = require('../config/sequelize'); // sequelize 인스턴스 경로에 맞게 수정하세요. +const { sequelize } = require('../config/sequelize'); const User = sequelize.define('User', { + id: { + type: DataTypes.BIGINT, // 수정: id 필드를 BIGINT로 설정 + autoIncrement: true, + primaryKey: true, + }, name: { type: DataTypes.STRING, // VARCHAR allowNull: false, @@ -21,4 +26,17 @@ const User = sequelize.define('User', { timestamps: true, // createdAt과 updatedAt 자동 관리 }); +User.associate = (models) => { + User.hasMany(models.Meeting, { + foreignKey: 'created_by', + as: 'createdMeetings', + }); + + User.hasMany(models.MeetingParticipant, { + foreignKey: 'user_id', + as: 'userMeetingParticipations', + }); +}; + + module.exports = User; diff --git a/routes/chat.js b/routes/chat.js deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/routes/chatRoute.js b/routes/chatRoute.js new file mode 100644 index 0000000000000000000000000000000000000000..6ae03f13f5e429e9c245c289d859400f99ac799a --- /dev/null +++ b/routes/chatRoute.js @@ -0,0 +1,14 @@ +const express = require('express'); +const router = express.Router(); +const chatController = require('../controllers/chatController'); + +router.post('/create-room', chatController.createChatRoom); +router.get('/rooms', chatController.getChatRooms); +router.post('/update-status', chatController.updateStatus); +router.post('/update-read-status', chatController.updateReadStatus); +router.get('/unread-messages/:nickname', chatController.getUnreadMessages); +router.get('/unread-count/:chatRoomId', chatController.getUnreadCount); +router.post('/update-status-and-logid', chatController.updateStatusAndLogId); +router.post('/update-read-log-id', chatController.updateReadLogId); + +module.exports = router; diff --git a/routes/meeting.js b/routes/meeting.js deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/routes/meetingRoute.js b/routes/meetingRoute.js new file mode 100644 index 0000000000000000000000000000000000000000..a2788cad0c127f0b8014ffe20ccca7f86b061b9b --- /dev/null +++ b/routes/meetingRoute.js @@ -0,0 +1,25 @@ +// routes/meetingRoutes.js + +const express = require('express'); +const router = express.Router(); +const { isLoggedIn } = require('../middlewares/auth'); +const MeetingController = require('../controllers/meetingController'); + +router.use(isLoggedIn); + +// 번개 모임 생성 +router.post('/', MeetingController.createMeeting); + +// 번개 모임 목록 조회 +router.get('/', MeetingController.getMeetings); + +// 번개 모임 마감 +router.put('/:meetingId/close', MeetingController.closeMeeting); + +// 번개 모임 참가 +router.post('/:meetingId/join', MeetingController.joinMeeting); + +// 번개 모임 상세 조회 +router.get('/:meetingId', MeetingController.getMeetingDetail); + +module.exports = router; \ No newline at end of file diff --git a/services/chatService.js b/services/chatService.js new file mode 100644 index 0000000000000000000000000000000000000000..4135d1581f9cf64efc6377c6d7cabb9313303a7f --- /dev/null +++ b/services/chatService.js @@ -0,0 +1,149 @@ +const ChatRoom = require('../models/chatRooms'); +const { v4: uuidv4 } = require('uuid'); + +class ChatService { + + // 채팅방 생성 + async createChatRoom({ meeting_id, participants }) { + try { + const chatRoomId = uuidv4(); + const newRoom = new ChatRoom({ + chatRoomId: chatRoomId, + meeting_id, + participants, + messages: [], + lastReadAt: participants.reduce((acc, user) => { + acc[user] = new Date(); + return acc; + }, {}), + lastReadLogId: participants.reduce((acc, user) => { + acc[user] = null; + return acc; + }, {}), + isOnline: participants.reduce((acc, user) => { + acc[user] = true; + return acc; + }, {}), + }); + + const joinMessage = { + message: `${participants[0]}님이 번개 모임을 생성했습니다.`, + timestamp: new Date(), + type: 'join', + }; + + newRoom.messages.push(joinMessage); + await newRoom.save(); + + return { success: true, chatRoomId }; + } catch (err) { + console.error('Error creating chat room:', err); + throw new Error('Failed to create chat room'); + } + } + + // 채팅방 목록 조회 + async getChatRooms() { + const rooms = await ChatRoom.find({}, { chatRoomId: 1, messages: { $slice: -1 } }); + return rooms.map(room => { + const lastMessage = room.messages[0] || {}; + return { + chatRoomId: room.chatRoomId, + lastMessage: { + sender: lastMessage.sender || '없음', + message: lastMessage.message || '메시지 없음', + timestamp: lastMessage.timestamp || null, + } + }; + }); + } + + // 사용자 상태 업데이트 + async updateStatus(chatRoomId, nickname, isOnline) { + await ChatRoom.updateOne( + { chatRoomId }, + { $set: { [`isOnline.${nickname}`]: isOnline } } + ); + } + + // 읽음 상태 업데이트 + async updateReadStatus(chatRoomId, nickname) { + const now = new Date(); + await ChatRoom.updateOne( + { chatRoomId }, + { $set: { [`lastReadAt.${nickname}`]: now } } + ); + } + + // 읽지 않은 메시지 조회 + async getUnreadMessages(nickname) { + const chatRooms = await ChatRoom.find({ participants: nickname }); + return await Promise.all(chatRooms.map(async (chatRoom) => { + const lastReadAt = chatRoom.lastReadAt.get(nickname) || new Date(0); + const unreadMessagesCount = chatRoom.messages.filter(message => + message.timestamp > lastReadAt + ).length; + return { + chatRoomId: chatRoom.chatRoomId, + unreadCount: unreadMessagesCount, + }; + })); + } + + // 읽지 않은 메시지 수 조회 + async getUnreadCount(chatRoomId) { + const chatRoom = await ChatRoom.findOne({ chatRoomId }); + if (!chatRoom) { + throw new Error('Chat room not found'); + } + + const unreadCounts = chatRoom.participants + .filter(user => chatRoom.lastReadLogId.get(user)) + .map(user => chatRoom.lastReadLogId.get(user)) + .reduce((acc, logId) => { + acc[logId] = (acc[logId] || 0) + 1; + return acc; + }, {}); + + let count = 0; + return Object.entries(unreadCounts) + .sort(([logId1], [logId2]) => logId1.localeCompare(logId2)) + .reduce((acc, [logId, value]) => { + count += value; + acc[count] = logId; + return acc; + }, {}); + } + + // 읽은 메시지 로그 ID 업데이트 + async updateReadLogId(chatRoomId, nickname, logId) { + await ChatRoom.updateOne( + { chatRoomId }, + { $set: { [`lastReadLogId.${nickname}`]: logId } } + ); + } + + // 상태와 로그 ID 동시 업데이트 + async updateStatusAndLogId(chatRoomId, nickname, isOnline, logId) { + let finalLogId = logId; + + if (!isOnline && logId === null) { + const chatRoom = await ChatRoom.findOne({ chatRoomId }); + if (chatRoom && chatRoom.messages.length > 0) { + finalLogId = chatRoom.messages[chatRoom.messages.length - 1]._id; + } + } + + await ChatRoom.updateOne( + { chatRoomId }, + { + $set: { + [`isOnline.${nickname}`]: isOnline, + [`lastReadLogId.${nickname}`]: isOnline ? null : finalLogId, + }, + } + ); + } +} + +module.exports = new ChatService(); diff --git a/services/meetingService.js b/services/meetingService.js new file mode 100644 index 0000000000000000000000000000000000000000..912a120a3881a985ac5e215da0a7fed6ba729bc5 --- /dev/null +++ b/services/meetingService.js @@ -0,0 +1,159 @@ +const { v4: uuidv4 } = require('uuid'); +const { Meeting, MeetingParticipant, User } = require('../models'); +const ChatRoom = require('../models/chatRooms'); +const chatController = require('../controllers/chatController'); +const MeetingResponse = require('../dtos/MeetingResponse'); +const MeetingDetailResponse = require('../dtos/MeetingDetailResponse'); + +class MeetingService { + async createMeeting(meetingData) { + const { title, description, start_time, end_time, location, deadline, type, created_by } = meetingData; + + const user = await User.findOne({ where: { id: created_by } }); + if (!user) { + throw new Error('사용자를 찾을 수 없습니다.'); + } + + const chatRoomData = { + participants: [user.name], + }; + const chatRoomResponse = await chatController.createChatRoomInternal(chatRoomData); + + if (!chatRoomResponse.success) { + throw new Error('채팅방 생성 실패'); + } + + const chatRoomId = chatRoomResponse.chatRoomId; + + const newMeeting = await Meeting.create({ + title, + description, + start_time, + end_time, + location, + deadline, + type, + created_by, + chatRoomId, + }); + + await MeetingParticipant.create({ + meeting_id: newMeeting.id, + user_id: created_by, + }); + + return { meeting_id: newMeeting.id, chatRoomId: chatRoomResponse.chatRoomId }; + } + + async getMeetings(userId) { + const meetings = await Meeting.findAll({ + attributes: ['id', 'title', 'description', 'start_time', 'end_time', 'location', 'deadline', 'type'], + include: [ + { + model: User, + as: 'creator', + attributes: ['name'], + }, + { + model: MeetingParticipant, + as: 'participants', + attributes: ['user_id'], + }, + ], + }); + + return meetings.map((meeting) => { + const creatorName = meeting.creator ? meeting.creator.name : 'Unknown'; + const isParticipant = meeting.participants.some(participant => participant.user_id === parseInt(userId, 10)); + + return new MeetingResponse( + meeting, + isParticipant, + false, + creatorName + ); + }); + } + + async closeMeeting(meetingId) { + const meeting = await Meeting.findByPk(meetingId); + if (!meeting) { + throw new Error('모임을 찾을 수 없습니다.'); + } + + if (meeting.type === 'CLOSE') { + throw new Error('이미 마감된 모임입니다.'); + } + + meeting.type = 'CLOSE'; + await meeting.save(); + return meeting; + } + + async joinMeeting(meetingId, userId) { + const meeting = await Meeting.findByPk(meetingId); + if (!meeting) { + throw new Error('모임을 찾을 수 없습니다.'); + } + + if(meeting.type === 'CLOSE') { + throw new Error('이미 마감된 모임입니다.'); + } + + if (new Date() > new Date(meeting.deadline)) { + throw new Error('참가 신청이 마감되었습니다.'); + } + + const existingParticipant = await MeetingParticipant.findOne({ + where: { meeting_id: meetingId, user_id: userId } + }); + + if (existingParticipant) { + throw new Error('이미 참가한 사용자입니다.'); + } + + await MeetingParticipant.create({ meeting_id: meetingId, user_id: userId }); + + const user = await User.findOne({ where: { id: userId } }); + const chatRoom = await ChatRoom.findOne({ meeting_id: meetingId }); + + if (chatRoom && !chatRoom.participants.includes(user.name)) { + chatRoom.participants.push(user.name); + chatRoom.isOnline.set(user.name, true); + chatRoom.lastReadAt.set(user.name, new Date()); + chatRoom.lastReadLogId.set(user.name, null); + await chatRoom.save(); + } + } + + async getMeetingDetail(meetingId) { + const meeting = await Meeting.findByPk(meetingId, { + include: [ + { + model: User, + as: 'creator', + attributes: ['name'] + }, + { + model: MeetingParticipant, + as: 'participants', + include: [ + { + model: User, + as: 'participantUser', + attributes: ['name', 'email'] + } + ] + } + ] + }); + + if (!meeting) { + throw new Error('모임을 찾을 수 없습니다.'); + } + + return new MeetingDetailResponse(meeting); + } +} + +module.exports = new MeetingService(); diff --git a/wsServer.js b/wsServer.js new file mode 100644 index 0000000000000000000000000000000000000000..3f303b5db4c2b4aadd965b7adead567e547a4efc --- /dev/null +++ b/wsServer.js @@ -0,0 +1,296 @@ +const http = require('http'); +const crypto = require('crypto'); +// const ChatRoom = require('./models/chatRoom.js'); +const mongoose = require('mongoose'); +const ChatRoom = require('./models/chatRooms'); + +// WebSocket 관련 데이터 +let clients = []; +let chatRooms = {}; + +// MongoDB 연결 설정 +async function connectMongoDB() { + try { + await mongoose.connect('mongodb://localhost:27017/chat', { + useNewUrlParser: true, + useUnifiedTopology: true, + }); + console.log('MongoDB에 성공적으로 연결되었습니다.'); + + // MongoDB 연결 성공 후 WebSocket 서버 시작 + startWebSocketServer(); + } catch (err) { + console.error('MongoDB 연결 실패:', err); + process.exit(1); + } +} + +// 채팅방 기록 불러오기 함수 +async function getChatHistory(chatRoomId) { + const chatRoom = await ChatRoom.findOne({ chatRoomId }); + return chatRoom ? chatRoom.messages : []; +} + +// WebSocket 서버 생성 및 핸드셰이크 처리 +function startWebSocketServer() { + const wsServer = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('WebSocket server is running'); + }); + + wsServer.on('upgrade', (req, socket, head) => { + const key = req.headers['sec-websocket-key']; + const acceptKey = generateAcceptValue(key); + const responseHeaders = [ + 'HTTP/1.1 101 Switching Protocols', + 'Upgrade: websocket', + 'Connection: Upgrade', + `Sec-WebSocket-Accept: ${acceptKey}` + ]; + socket.write(responseHeaders.join('\r\n') + '\r\n\r\n'); + + // 클라이언트를 clients 배열에 추가 + clients.push(socket); + + let chatRoomId = null; + let nickname = null; + + socket.on('data', async buffer => { + let message; + try { + message = parseMessage(buffer); + const parsedData = JSON.parse(message); + const { type, chatRoomId: clientChatRoomId, nickname: clientNickname, text } = parsedData; + + console.log('서버에서 수신한 메시지:', { type, clientChatRoomId, clientNickname, text }); + + if (type === 'join' || type === 'leave') { + await ChatRoom.updateOne( + { chatRoomId: clientChatRoomId }, + { $set: { [`isOnline.${clientNickname}`]: type === 'join' } } + ); + + const statusMessage = { + type: 'status', + chatRoomId: clientChatRoomId, + nickname: clientNickname, + isOnline: type === 'join', + }; + + clients.forEach(client => { + client.write(constructReply(JSON.stringify(statusMessage))); + }); + } + + if (type === 'join') { + chatRoomId = clientChatRoomId; + nickname = clientNickname; + console.log("join시 chatRoomId", chatRoomId); + console.log("join시 nickname", nickname); + + await ChatRoom.updateOne( + { chatRoomId }, + { + $set: { + [`isOnline.${nickname}`]: true, + [`lastReadLogId.${nickname}`]: null, + }, + } + ); + + if (!chatRooms[chatRoomId]) { + chatRooms[chatRoomId] = []; + } + + const chatRoom = await ChatRoom.findOne({ chatRoomId }); + console.log("join시 chatRoom", chatRoom); + if (!chatRoom) { + console.error(`ChatRoom을 찾을 수 없습니다: chatRoomId = ${chatRoomId}`); + } else { + console.log(`ChatRoom 조회 성공: ${chatRoom}`); + } + + const isAlreadyParticipant = chatRoom.participants.includes(nickname); + if (!isAlreadyParticipant) { + const joinMessage = { + message: `${nickname}님이 참가했습니다.`, + timestamp: new Date(), + type: 'join' + }; + + chatRooms[chatRoomId].push(joinMessage); + + await ChatRoom.updateOne({ chatRoomId }, { + $push: { messages: joinMessage, participants: nickname } + }); + + clients.forEach(client => { + client.write(constructReply(JSON.stringify(joinMessage))); + }); + } else { + console.log(`${nickname}은 이미 채팅방에 참가 중입니다.`); + } + + try { + const previousMessages = await getChatHistory(chatRoomId); + if (previousMessages.length > 0) { + socket.write(constructReply(JSON.stringify({ type: 'previousMessages', messages: previousMessages }))); + console.log(`이전 메시지 전송: ${previousMessages.length}개`); + } + } catch (err) { + console.error('이전 채팅 기록 불러오기 중 오류 발생:', err); + } + + } else if (type === 'message') { + const chatMessage = { + message: text, + timestamp: new Date(), + type: 'message', + sender: nickname + }; + + + chatRooms[chatRoomId].push(chatMessage); + + try { + // 새로운 메시지를 messages 배열에 추가 + const updatedChatRoom = await ChatRoom.findOneAndUpdate( + { chatRoomId }, + { $push: { messages: chatMessage } }, + { new: true, fields: { "messages": { $slice: -1 } } } // 마지막 추가된 메시지만 가져옴 + ); + + // 마지막에 추가된 메시지의 _id를 가져오기 + const savedMessage = updatedChatRoom.messages[updatedChatRoom.messages.length - 1]; + + // 새로운 메시지 전송: 클라이언트로 메시지 브로드캐스트 + const messageData = { + type: 'message', + chatRoomId, + sender: nickname, + message: text, + timestamp: chatMessage.timestamp, + _id: savedMessage._id // 저장된 메시지의 _id 사용 + }; + + clients.forEach(client => { + client.write(constructReply(JSON.stringify(messageData))); + console.log('채팅 메시지 전송:', messageData); + }); + + } catch (err) { + console.error('MongoDB 채팅 메시지 저장 오류:', err); + } + } else if (type === 'leave') { + const leaveMessage = { message: `${nickname}님이 퇴장했습니다.`, timestamp: new Date() }; + chatRooms[chatRoomId].push(leaveMessage); + + await ChatRoom.updateOne( + { chatRoomId }, + { $set: { [`isOnline.${nickname}`]: false } } + ); + + await ChatRoom.updateOne({ chatRoomId }, { + $push: { messages: leaveMessage }, + $pull: { participants: nickname } + }); + + clients.forEach(client => { + client.write(constructReply(JSON.stringify(leaveMessage))); + }); + + clients = clients.filter(client => client !== socket); + } + } catch (err) { + console.error('메시지 처리 중 오류 발생:', err); + } + }); + + socket.on('close', async () => { + if (nickname && chatRoomId) { + await ChatRoom.updateOne( + { chatRoomId }, + { $set: { [`isOnline.${nickname}`]: false } } + ); + + const statusMessage = { + type: 'status', + chatRoomId, + nickname, + isOnline: false, + }; + + clients.forEach(client => { + client.write(constructReply(JSON.stringify(statusMessage))); + }); + } + }); + + socket.on('error', (err) => { + console.error(`WebSocket error: ${err}`); + clients = clients.filter(client => client !== socket); + }); + }); + + wsServer.listen(8081, () => { + console.log('WebSocket 채팅 서버가 8081 포트에서 실행 중입니다.'); + }); +} + +// Sec-WebSocket-Accept 헤더 값 생성 -> env처리 +function generateAcceptValue(key) { + return crypto.createHash('sha1').update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 'binary').digest('base64'); +} + +// WebSocket 메시지 파싱 함수 +function parseMessage(buffer) { + const byteArray = [...buffer]; + const secondByte = byteArray[1]; + let length = secondByte & 127; + let maskStart = 2; + + if (length === 126) { + length = (byteArray[2] << 8) + byteArray[3]; + maskStart = 4; + } else if (length === 127) { + length = 0; + for (let i = 0; i < 8; i++) { + length = (length << 8) + byteArray[2 + i]; + } + maskStart = 10; + } + + const dataStart = maskStart + 4; + const mask = byteArray.slice(maskStart, dataStart); + const data = byteArray.slice(dataStart, dataStart + length).map((byte, i) => byte ^ mask[i % 4]); + + return new TextDecoder('utf-8').decode(Uint8Array.from(data)); +} + +// 클라이언트 메시지 응답 생성 함수 +function constructReply(message) { + const messageBuffer = Buffer.from(message, 'utf-8'); + const length = messageBuffer.length; + const reply = [0x81]; + if (length < 126) { + reply.push(length); + } else if (length < 65536) { + reply.push(126, (length >> 8) & 255, length & 255); + } else { + reply.push( + 127, + (length >> 56) & 255, + (length >> 48) & 255, + (length >> 40) & 255, + (length >> 32) & 255, + (length >> 24) & 255, + (length >> 16) & 255, + (length >> 8) & 255, + length & 255 + ); + } + return Buffer.concat([Buffer.from(reply), messageBuffer]); +} + +// MongoDB 연결 후 WebSocket 서버 시작 +connectMongoDB(); \ No newline at end of file