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