From 76d788065b1f7d894c7c3613c302b4e5af78dbe3 Mon Sep 17 00:00:00 2001 From: tpgus2603 <kakaneymar2424@gmail.com> Date: Mon, 25 Nov 2024 01:47:20 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EB=AF=B8=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=9D=B8=EC=9B=90=EC=A0=9C=ED=95=9C,=ED=98=84=EC=9E=AC?= =?UTF-8?q?=EC=9D=B8=EC=9B=90=20=EC=BB=AC=EB=9F=BC=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B4=80=EB=A0=A8=EB=A1=9C=EC=A7=81=EC=B6=94?= =?UTF-8?q?=EA=B0=80(#16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- models/Invite.js | 37 ++++ models/meeting.js | 74 ++++--- services/meetingService.js | 383 ++++++++++++++++++++++++++----------- 3 files changed, 356 insertions(+), 138 deletions(-) create mode 100644 models/Invite.js diff --git a/models/Invite.js b/models/Invite.js new file mode 100644 index 0000000..a49c264 --- /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 0616319..9946d1e 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/services/meetingService.js b/services/meetingService.js index 67179f9..44ed519 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'); @@ -30,7 +29,7 @@ class MeetingService { /** * 번개 모임 생성 * @param {object} meetingData - 모임 생성 데이터 - * @returns {Promise<object>} - 생성된 모임 ID와 채팅방 ID + * @returns {Promise<object>} - 생성된 모임 ID와 채팅방 ID, 초대된 친구 ID 목록 */ async createMeeting(meetingData) { // DTO를 사용하여 요청 데이터 검증 @@ -46,6 +45,7 @@ class MeetingService { time_idx_deadline, type, created_by, + max_num, } = meetingData; // 사용자 존재 여부 확인 @@ -81,6 +81,8 @@ class MeetingService { type, created_by, chatRoomId, + max_num, // max_num 추가 + cur_num: 1, // 생성자 자신 포함 }, { transaction } ); @@ -99,7 +101,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,49 +111,205 @@ 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; } + 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; + } + + /** + * 번개 모임 참가 + * @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) => { + // 현재 인원 수 확인 + 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 }); + }); + } + /** * 번개 모임 목록 조회 * @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'], // 미팅 생성자의 이름만 필요 - }, - ], - }); - - return meetings.map((meeting) => { - const creatorName = meeting.creator ? meeting.creator.name : 'Unknown'; - return new MeetingResponseDTO(meeting, true, false, creatorName); - }); - } + 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: [], // MeetingParticipant 테이블의 데이터는 필요 없으므로 제외 + }, + { + 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); + }); + } /** * 번개 모임 마감 @@ -164,11 +321,9 @@ class MeetingService { if (!meeting) { throw new Error('모임을 찾을 수 없습니다.'); } - if (meeting.type === 'CLOSE') { throw new Error('이미 마감된 모임입니다.'); } - meeting.type = 'CLOSE'; await meeting.save(); return meeting; @@ -186,73 +341,84 @@ class MeetingService { 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 }); }); } @@ -261,35 +427,34 @@ class MeetingService { * @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(); -- GitLab