diff --git a/controllers/inviteController.js b/controllers/inviteController.js new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/models/Invite.js b/models/Invite.js new file mode 100644 index 0000000000000000000000000000000000000000..a49c264ba83c345a06cd6e0000f425d354a411e2 --- /dev/null +++ b/models/Invite.js @@ -0,0 +1,37 @@ +// models/Invite.js +const { DataTypes } = require('sequelize'); +const sequelize = require('../config/sequelize'); +const User = require('./User'); +const Meeting = require('./Meeting'); + +const Invite = sequelize.define('Invite', { + status: { + type: DataTypes.ENUM('PENDING', 'ACCEPTED', 'DECLINED'), + allowNull: false, + defaultValue: 'PENDING', + }, +}, { + tableName: 'Invites', + timestamps: true, + underscored: true, + indexes: [ + { + unique: true, + fields: ['meeting_id', 'invitee_id'] + }, + { + fields: ['status'] + } + ] +}); + +// 관계 설정 +// Invite.belongsTo(Meeting, { foreignKey: 'meeting_id', as: 'meeting' }); +// Invite.belongsTo(User, { foreignKey: 'inviter_id', as: 'inviter' }); // 초대한 사용자 +// Invite.belongsTo(User, { foreignKey: 'invitee_id', as: 'invitee' }); // 초대받은 사용자 + +// User.hasMany(Invite, { foreignKey: 'inviter_id', as: 'sentInvites' }); // 보낸 초대 목록 +// User.hasMany(Invite, { foreignKey: 'invitee_id', as: 'receivedInvites' }); // 받은 초대 목록 +// Meeting.hasMany(Invite, { foreignKey: 'meeting_id', as: 'invites' }); // 해당 미팅의 모든 초대 + +module.exports = Invite; \ No newline at end of file diff --git a/models/meeting.js b/models/meeting.js index 0616319a80c27659ae677d8f1c95113e8982dd04..9946d1e2c6e9c0fef889c0e848e6be5a1fe4b55d 100644 --- a/models/meeting.js +++ b/models/meeting.js @@ -1,37 +1,53 @@ // models/Meeting.js const { DataTypes } = require('sequelize'); -const sequelize = require('../config/sequelize'); -const User = require('./user'); +const sequelize = require('../config/sequelize'); +const User = require('./User'); const Meeting = sequelize.define('Meeting', { - title: { - type: DataTypes.STRING, - allowNull: false, - }, - description: { - type: DataTypes.TEXT, - }, - time_idx_start: { - type: DataTypes.INTEGER, - allowNull: false, - }, - time_idx_end: { - type: DataTypes.INTEGER, - allowNull: false, - }, - location: { - type: DataTypes.STRING, - }, - time_idx_deadline: { - type: DataTypes.INTEGER, - }, - type: { - type: DataTypes.ENUM('OPEN', 'CLOSE'), - allowNull: false, - }, + title: { + type: DataTypes.STRING, + allowNull: false, + }, + description: { + type: DataTypes.TEXT, + }, + time_idx_start: { + type: DataTypes.INTEGER, + allowNull: false, + }, + time_idx_end: { + type: DataTypes.INTEGER, + allowNull: false, + }, + location: { + type: DataTypes.STRING, + }, + time_idx_deadline: { + type: DataTypes.INTEGER, + }, + type: { + type: DataTypes.ENUM('OPEN', 'CLOSE'), + allowNull: false, + defaultValue: 'OPEN', + }, + chatRoomId: { + type: DataTypes.UUID, + allowNull: false, + }, + max_num: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 10, // 기본값 설정 (필요에 따라 조정) + }, + cur_num: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 1, // 생성자 자신 포함 + }, }, { - tableName: 'Meetings', - timestamps: false, + tableName: 'Meetings', + timestamps: true, + underscored: true, }); module.exports = Meeting; diff --git a/routes/inviteRoutes.js b/routes/inviteRoutes.js new file mode 100644 index 0000000000000000000000000000000000000000..cef8ad9a65e62f1112487bb1cc20973782c4cea1 --- /dev/null +++ b/routes/inviteRoutes.js @@ -0,0 +1,32 @@ +// routes/inviteRoutes.js +const express = require('express'); +const router = express.Router(); +const inviteController = require('../controllers/inviteController'); +const { isLoggedIn } = require('../middlewares/auth'); + + +router.use(isLoggedIn); +// 초대 응답 +router.post('/respond', async (req, res) => { + const { inviteId, response } = req.body; + const userId = req.user.id; // 인증된 사용자 ID + try { + const result = await inviteController.respondToInvite(inviteId, userId, response); + res.status(200).json({ success: true, result }); + } catch (error) { + res.status(400).json({ success: false, message: error.message }); + } +}); + +// 받은 초대 조회 +router.get('/received', async (req, res) => { + const userId = req.user.id; // 인증된 사용자 ID + try { + const invites = await inviteController.getReceivedInvites(userId); + res.status(200).json({ success: true, invites }); + } catch (error) { + res.status(400).json({ success: false, message: error.message }); + } +}); + +module.exports = router; diff --git a/services/meetingService.js b/services/meetingService.js index 67179f935a056b999b5b3fe909955ac8f740dae4..389942dd439ba2b9e5c61bd1dcc61708d3beedf2 100644 --- a/services/meetingService.js +++ b/services/meetingService.js @@ -2,9 +2,8 @@ const { v4: uuidv4 } = require('uuid'); const { Op } = require('sequelize'); const sequelize = require('../config/sequelize'); // 트랜잭션 관리를 위해 sequelize 인스턴스 필요 -const { Meeting, MeetingParticipant, User, Schedule } = require('../models'); +const { Meeting, MeetingParticipant, User, Schedule, Invite, Friend } = require('../models'); const ChatRooms = require('../models/ChatRooms'); - const MeetingResponseDTO = require('../dtos/MeetingResponseDTO'); const MeetingDetailResponseDTO = require('../dtos/MeetingDetailResponseDTO'); const CreateMeetingRequestDTO = require('../dtos/CreateMeetingRequestDTO'); @@ -27,11 +26,6 @@ class MeetingService { return totalIdx; } - /** - * 번개 모임 생성 - * @param {object} meetingData - 모임 생성 데이터 - * @returns {Promise<object>} - 생성된 모임 ID와 채팅방 ID - */ async createMeeting(meetingData) { // DTO를 사용하여 요청 데이터 검증 const createMeetingDTO = new CreateMeetingRequestDTO(meetingData); @@ -46,6 +40,7 @@ class MeetingService { time_idx_deadline, type, created_by, + max_num, } = meetingData; // 사용자 존재 여부 확인 @@ -81,6 +76,8 @@ class MeetingService { type, created_by, chatRoomId, + max_num, // max_num 추가 + cur_num: 1, // 생성자 자신 포함 }, { transaction } ); @@ -99,7 +96,6 @@ class MeetingService { for (let idx = time_idx_start; idx <= time_idx_end; idx++) { events.push({ time_idx: idx }); } - await ScheduleService.createSchedules( { userId: created_by, @@ -110,186 +106,326 @@ class MeetingService { transaction ); - return { meeting_id: newMeeting.id, chatRoomId }; + // 친구 초대 로직 호출 + const invitedFriendIds = await this.sendInvites({ + meetingId: newMeeting.id, + creatorId: created_by, + time_idx_start, + time_idx_end, + }, transaction); + + return { meeting_id: newMeeting.id, chatRoomId, invitedFriendIds }; }); return result; } - /** - * 번개 모임 목록 조회 - * @param {number} userId - 사용자 ID - * @returns {Promise<Array<MeetingResponseDTO>>} - 모임 목록 DTO 배열 - */ - async getMeetings(userId) { - const meetings = await Meeting.findAll({ - attributes: [ - 'id', - 'title', - 'description', - 'time_idx_start', - 'time_idx_end', - 'location', - 'time_idx_deadline', - 'type', - ], - include: [ - { - model: MeetingParticipant, - as: 'participants', - where: { user_id: userId }, // userId와 매핑된 미팅만 가져옴 - attributes: [], // MeetingParticipant 테이블의 데이터는 필요 없으므로 제외 - }, - { - model: User, - as: 'creator', - attributes: ['name'], // 미팅 생성자의 이름만 필요 - }, - ], - }); + async sendInvites({ meetingId, creatorId, time_idx_start, time_idx_end }, transaction) { + // 1. 친구 목록 가져오기 (ACCEPTED 상태) + const friends = await Friend.findAll({ + where: { + [Op.or]: [ + { requester_id: creatorId, status: 'ACCEPTED' }, + { receiver_id: creatorId, status: 'ACCEPTED' }, + ], + }, + transaction, + }); + + const friendIds = friends.map(friend => + friend.requester_id === creatorId ? friend.receiver_id : friend.requester_id + ); + + if (friendIds.length === 0) { + // 친구가 없거나 모든 친구가 초대받지 못함 + return []; + } + const schedules = await Schedule.findAll({ + where: { + user_id: { [Op.in]: friendIds }, + time_idx: { + [Op.between]: [time_idx_start, time_idx_end], + }, + }, + transaction, + }); + + // 스케줄이 겹치는 친구 ID를 추출 + const conflictedFriendIds = schedules.map(schedule => schedule.user_id); + + // 스케줄이 겹치지 않는 친구 ID 필터링 + const availableFriendIds = friendIds.filter(friendId => !conflictedFriendIds.includes(friendId)); + + if (availableFriendIds.length === 0) { + // 스케줄이 겹치는 친구가 모두 있음 + return []; + } + const invitePromises = availableFriendIds.map(inviteeId => { + return Invite.create({ + meeting_id: meetingId, + inviter_id: creatorId, + invitee_id: inviteeId, + status: 'PENDING', + }, { transaction }); + }); + + await Promise.all(invitePromises); + + return availableFriendIds; + } + - return meetings.map((meeting) => { - const creatorName = meeting.creator ? meeting.creator.name : 'Unknown'; - return new MeetingResponseDTO(meeting, true, false, creatorName); - }); - } + async joinMeeting(meetingId, userId) { + const meeting = await Meeting.findByPk(meetingId); + console.log(`참여하려는 모임: ${JSON.stringify(meeting)}`); + if (!meeting) { + throw new Error('모임을 찾을 수 없습니다.'); + } + if (meeting.type === 'CLOSE') { + throw new Error('이미 마감된 모임입니다.'); + } + if (meeting.time_idx_deadline !== undefined) { + const currentTimeIdx = this.getCurrentTimeIdx(); // 현재 시간 인덱스 + if (currentTimeIdx >= meeting.time_idx_deadline) { + throw new Error('참가 신청이 마감되었습니다.'); + } + } + const existingParticipant = await MeetingParticipant.findOne({ + where: { meeting_id: meetingId, user_id: userId }, + }); + if (existingParticipant) { + throw new Error('이미 참가한 사용자입니다.'); + } - /** - * 번개 모임 마감 - * @param {number} meetingId - 모임 ID - * @returns {Promise<Meeting>} - 마감된 모임 객체 - */ + // 트랜잭션을 사용하여 참가자 추가 및 스케줄 업데이트를 원자적으로 처리 + await sequelize.transaction(async (transaction) => { + if (meeting.cur_num >= meeting.max_num) { + throw new Error("모임 인원이 모두 찼습니다."); + } + // 스케줄 충돌 확인 + const hasConflict = await ScheduleService.checkScheduleOverlapByTime( + userId, + meeting.time_idx_start, + meeting.time_idx_end, + transaction + ); + console.log(`스케줄 충돌 결과: ${hasConflict}`); + if (hasConflict) { + throw new Error("스케줄이 겹칩니다. 다른 모임에 참가하세요."); + } + + await MeetingParticipant.create( + { meeting_id: meetingId, user_id: userId }, + { transaction } + ); + + // 스케줄 생성 (모임 시간 범위 내 모든 time_idx에 대해 생성) + const events = []; + for ( + let idx = meeting.time_idx_start; + idx <= meeting.time_idx_end; + idx++ + ) { + events.push({ time_idx: idx }); + } + await ScheduleService.createSchedules( + { + userId: userId, + title: `번개 모임: ${meeting.title}`, + is_fixed: true, + events: events, + }, + transaction + ); + + // 채팅방 참가 (MongoDB) + const user = await User.findOne({ + where: { id: userId }, + transaction, + }); + const chatRoom = await ChatRooms.findOne({ + chatRoomId: meeting.chatRoomId, + }); + 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(); + } + + // 현재 인원 수 증가 + await meeting.increment("cur_num", { by: 1, transaction }); + }); + } + + + async getMeetings(userId) { + const meetings = await Meeting.findAll({ + attributes: [ + 'id', + 'title', + 'description', + 'time_idx_start', + 'time_idx_end', + 'location', + 'time_idx_deadline', + 'type', + 'max_num', + 'cur_num', + ], + include: [ + { + model: MeetingParticipant, + as: 'participants', + where: { user_id: userId }, // userId와 매핑된 미팅만 가져옴 + attributes: [], + }, + { + model: User, + as: 'creator', + attributes: ['name'], // 미팅 생성자의 이름만 필요 + }, + ], + }); + + return meetings.map((meeting) => { + const creatorName = meeting.creator ? meeting.creator.name : 'Unknown'; + return new MeetingResponseDTO(meeting, true, 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; } - /** - * 번개 모임 참가 - * @param {number} meetingId - 모임 ID - * @param {number} userId - 사용자 ID - * @returns {Promise<void>} - */ + async joinMeeting(meetingId, userId) { const meeting = await Meeting.findByPk(meetingId); console.log(`참여하려는 모임: ${JSON.stringify(meeting)}`); if (!meeting) { throw new Error('모임을 찾을 수 없습니다.'); } - if (meeting.type === 'CLOSE') { throw new Error('이미 마감된 모임입니다.'); } - if (meeting.time_idx_deadline !== undefined) { const currentTimeIdx = this.getCurrentTimeIdx(); // 현재 시간 인덱스 if (currentTimeIdx >= meeting.time_idx_deadline) { throw new Error('참가 신청이 마감되었습니다.'); } } - const existingParticipant = await MeetingParticipant.findOne({ where: { meeting_id: meetingId, user_id: userId }, }); - if (existingParticipant) { throw new Error('이미 참가한 사용자입니다.'); } // 트랜잭션을 사용하여 참가자 추가 및 스케줄 업데이트를 원자적으로 처리 await sequelize.transaction(async (transaction) => { - // 스케줄 충돌 확인 - const hasConflict = await ScheduleService.checkScheduleOverlapByTime( - userId, - meeting.time_idx_start, - meeting.time_idx_end, - transaction - ); - console.log(`스케줄 충돌 결과: ${hasConflict}`); - if (hasConflict) { - throw new Error('스케줄이 겹칩니다. 다른 모임에 참가하세요.'); - } + // 스케줄 충돌 확인 + // 현재 인원 수 확인 + if (meeting.cur_num >= meeting.max_num) { + throw new Error("모임 인원이 모두 찼습니다."); + } - // 참가자 추가 - await MeetingParticipant.create( - { meeting_id: meetingId, user_id: userId }, - { transaction } - ); + const hasConflict = await ScheduleService.checkScheduleOverlapByTime( + userId, + meeting.time_idx_start, + meeting.time_idx_end, + transaction + ); + console.log(`스케줄 충돌 결과: ${hasConflict}`); + if (hasConflict) { + throw new Error("스케줄이 겹칩니다. 다른 모임에 참가하세요."); + } - // 스케줄 생성 (모임 시간 범위 내 모든 time_idx에 대해 생성) - const events = []; - for (let idx = meeting.time_idx_start; idx <= meeting.time_idx_end; idx++) { - events.push({ time_idx: idx }); - } + // 참가자 추가 + await MeetingParticipant.create( + { meeting_id: meetingId, user_id: userId }, + { transaction } + ); - await ScheduleService.createSchedules( - { - userId: userId, - title: `번개 모임: ${meeting.title}`, - is_fixed: true, - events: events, - }, - transaction - ); + // 스케줄 생성 (모임 시간 범위 내 모든 time_idx에 대해 생성) + const events = []; + for ( + let idx = meeting.time_idx_start; + idx <= meeting.time_idx_end; + idx++ + ) { + events.push({ time_idx: idx }); + } + await ScheduleService.createSchedules( + { + userId: userId, + title: `번개 모임: ${meeting.title}`, + is_fixed: true, + events: events, + }, + transaction + ); - // 채팅방 참가 (MongoDB) - const user = await User.findOne({ where: { id: userId }, transaction }); - const chatRoom = await ChatRooms.findOne({ chatRoomId: meeting.chatRoomId }); + // 채팅방 참가 (MongoDB) + const user = await User.findOne({ + where: { id: userId }, + transaction, + }); + const chatRoom = await ChatRooms.findOne({ + chatRoomId: meeting.chatRoomId, + }); + 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(); + } - 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(); - } + // 현재 인원 수 증가 + await meeting.increment("cur_num", { by: 1, transaction }); }); } - /** - * 번개 모임 상세 조회 - * @param {number} meetingId - 모임 ID - * @returns {Promise<MeetingDetailResponseDTO>} - 모임 상세 DTO - */ - // services/meetingService.js - async getMeetingDetail(meetingId) { - const meeting = await Meeting.findByPk(meetingId, { - include: [ - { - model: User, - as: "creator", - attributes: ["name"], - }, - { - model: MeetingParticipant, - as: "participants", - include: [ - { - model: User, - as: "user", // 'participantUser'에서 'user'로 수정 - attributes: ["name", "email"], - }, - ], - }, - ], - }); + + async getMeetingDetail(meetingId) { + const meeting = await Meeting.findByPk(meetingId, { + include: [ + { + model: User, + as: "creator", + attributes: ["name"], + }, + { + model: MeetingParticipant, + as: "participants", + include: [ + { + model: User, + as: "user", // 'participantUser'에서 'user'로 수정 + attributes: ["name", "email"], + }, + ], + }, + ], + }); - if (!meeting) { - throw new Error("모임을 찾을 수 없습니다."); - } + if (!meeting) { + throw new Error("모임을 찾을 수 없습니다."); + } - return new MeetingDetailResponseDTO(meeting); - } + return new MeetingDetailResponseDTO(meeting); + } } module.exports = new MeetingService();