Skip to content
Snippets Groups Projects
Commit 40950690 authored by 세현 임's avatar 세현 임
Browse files

[#16] 미팅방 컬럼 추가 및 초대로직

parents 9a073dfb 82f600bc
No related branches found
No related tags found
2 merge requests!31Develop,!24[#16] 미팅방 컬럼 추가 및 초대로직
// 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
// models/Meeting.js // models/Meeting.js
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
const sequelize = require('../config/sequelize'); const sequelize = require('../config/sequelize');
const User = require('./user'); const User = require('./User');
const Meeting = sequelize.define('Meeting', { const Meeting = sequelize.define('Meeting', {
title: { title: {
...@@ -28,10 +28,26 @@ const Meeting = sequelize.define('Meeting', { ...@@ -28,10 +28,26 @@ const Meeting = sequelize.define('Meeting', {
type: { type: {
type: DataTypes.ENUM('OPEN', 'CLOSE'), type: DataTypes.ENUM('OPEN', 'CLOSE'),
allowNull: false, 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', tableName: 'Meetings',
timestamps: false, timestamps: true,
underscored: true,
}); });
module.exports = Meeting; module.exports = Meeting;
// 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;
...@@ -2,9 +2,8 @@ ...@@ -2,9 +2,8 @@
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
const { Op } = require('sequelize'); const { Op } = require('sequelize');
const sequelize = require('../config/sequelize'); // 트랜잭션 관리를 위해 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 ChatRooms = require('../models/ChatRooms');
const MeetingResponseDTO = require('../dtos/MeetingResponseDTO'); const MeetingResponseDTO = require('../dtos/MeetingResponseDTO');
const MeetingDetailResponseDTO = require('../dtos/MeetingDetailResponseDTO'); const MeetingDetailResponseDTO = require('../dtos/MeetingDetailResponseDTO');
const CreateMeetingRequestDTO = require('../dtos/CreateMeetingRequestDTO'); const CreateMeetingRequestDTO = require('../dtos/CreateMeetingRequestDTO');
...@@ -27,11 +26,6 @@ class MeetingService { ...@@ -27,11 +26,6 @@ class MeetingService {
return totalIdx; return totalIdx;
} }
/**
* 번개 모임 생성
* @param {object} meetingData - 모임 생성 데이터
* @returns {Promise<object>} - 생성된 모임 ID와 채팅방 ID
*/
async createMeeting(meetingData) { async createMeeting(meetingData) {
// DTO를 사용하여 요청 데이터 검증 // DTO를 사용하여 요청 데이터 검증
const createMeetingDTO = new CreateMeetingRequestDTO(meetingData); const createMeetingDTO = new CreateMeetingRequestDTO(meetingData);
...@@ -46,6 +40,7 @@ class MeetingService { ...@@ -46,6 +40,7 @@ class MeetingService {
time_idx_deadline, time_idx_deadline,
type, type,
created_by, created_by,
max_num,
} = meetingData; } = meetingData;
// 사용자 존재 여부 확인 // 사용자 존재 여부 확인
...@@ -81,6 +76,8 @@ class MeetingService { ...@@ -81,6 +76,8 @@ class MeetingService {
type, type,
created_by, created_by,
chatRoomId, chatRoomId,
max_num, // max_num 추가
cur_num: 1, // 생성자 자신 포함
}, },
{ transaction } { transaction }
); );
...@@ -99,7 +96,6 @@ class MeetingService { ...@@ -99,7 +96,6 @@ class MeetingService {
for (let idx = time_idx_start; idx <= time_idx_end; idx++) { for (let idx = time_idx_start; idx <= time_idx_end; idx++) {
events.push({ time_idx: idx }); events.push({ time_idx: idx });
} }
await ScheduleService.createSchedules( await ScheduleService.createSchedules(
{ {
userId: created_by, userId: created_by,
...@@ -110,17 +106,160 @@ class MeetingService { ...@@ -110,17 +106,160 @@ class MeetingService {
transaction 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; return result;
} }
/** async sendInvites({ meetingId, creatorId, time_idx_start, time_idx_end }, transaction) {
* 번개 모임 목록 조회 // 1. 친구 목록 가져오기 (ACCEPTED 상태)
* @param {number} userId - 사용자 ID const friends = await Friend.findAll({
* @returns {Promise<Array<MeetingResponseDTO>>} - 모임 목록 DTO 배열 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;
}
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 });
});
}
async getMeetings(userId) { async getMeetings(userId) {
const meetings = await Meeting.findAll({ const meetings = await Meeting.findAll({
attributes: [ attributes: [
...@@ -132,13 +271,15 @@ class MeetingService { ...@@ -132,13 +271,15 @@ class MeetingService {
'location', 'location',
'time_idx_deadline', 'time_idx_deadline',
'type', 'type',
'max_num',
'cur_num',
], ],
include: [ include: [
{ {
model: MeetingParticipant, model: MeetingParticipant,
as: 'participants', as: 'participants',
where: { user_id: userId }, // userId와 매핑된 미팅만 가져옴 where: { user_id: userId }, // userId와 매핑된 미팅만 가져옴
attributes: [], // MeetingParticipant 테이블의 데이터는 필요 없으므로 제외 attributes: [],
}, },
{ {
model: User, model: User,
...@@ -154,54 +295,39 @@ class MeetingService { ...@@ -154,54 +295,39 @@ class MeetingService {
}); });
} }
/**
* 번개 모임 마감
* @param {number} meetingId - 모임 ID
* @returns {Promise<Meeting>} - 마감된 모임 객체
*/
async closeMeeting(meetingId) { async closeMeeting(meetingId) {
const meeting = await Meeting.findByPk(meetingId); const meeting = await Meeting.findByPk(meetingId);
if (!meeting) { if (!meeting) {
throw new Error('모임을 찾을 수 없습니다.'); throw new Error('모임을 찾을 수 없습니다.');
} }
if (meeting.type === 'CLOSE') { if (meeting.type === 'CLOSE') {
throw new Error('이미 마감된 모임입니다.'); throw new Error('이미 마감된 모임입니다.');
} }
meeting.type = 'CLOSE'; meeting.type = 'CLOSE';
await meeting.save(); await meeting.save();
return meeting; return meeting;
} }
/**
* 번개 모임 참가
* @param {number} meetingId - 모임 ID
* @param {number} userId - 사용자 ID
* @returns {Promise<void>}
*/
async joinMeeting(meetingId, userId) { async joinMeeting(meetingId, userId) {
const meeting = await Meeting.findByPk(meetingId); const meeting = await Meeting.findByPk(meetingId);
console.log(`참여하려는 모임: ${JSON.stringify(meeting)}`); console.log(`참여하려는 모임: ${JSON.stringify(meeting)}`);
if (!meeting) { if (!meeting) {
throw new Error('모임을 찾을 수 없습니다.'); throw new Error('모임을 찾을 수 없습니다.');
} }
if (meeting.type === 'CLOSE') { if (meeting.type === 'CLOSE') {
throw new Error('이미 마감된 모임입니다.'); throw new Error('이미 마감된 모임입니다.');
} }
if (meeting.time_idx_deadline !== undefined) { if (meeting.time_idx_deadline !== undefined) {
const currentTimeIdx = this.getCurrentTimeIdx(); // 현재 시간 인덱스 const currentTimeIdx = this.getCurrentTimeIdx(); // 현재 시간 인덱스
if (currentTimeIdx >= meeting.time_idx_deadline) { if (currentTimeIdx >= meeting.time_idx_deadline) {
throw new Error('참가 신청이 마감되었습니다.'); throw new Error('참가 신청이 마감되었습니다.');
} }
} }
const existingParticipant = await MeetingParticipant.findOne({ const existingParticipant = await MeetingParticipant.findOne({
where: { meeting_id: meetingId, user_id: userId }, where: { meeting_id: meetingId, user_id: userId },
}); });
if (existingParticipant) { if (existingParticipant) {
throw new Error('이미 참가한 사용자입니다.'); throw new Error('이미 참가한 사용자입니다.');
} }
...@@ -209,6 +335,11 @@ class MeetingService { ...@@ -209,6 +335,11 @@ class MeetingService {
// 트랜잭션을 사용하여 참가자 추가 및 스케줄 업데이트를 원자적으로 처리 // 트랜잭션을 사용하여 참가자 추가 및 스케줄 업데이트를 원자적으로 처리
await sequelize.transaction(async (transaction) => { await sequelize.transaction(async (transaction) => {
// 스케줄 충돌 확인 // 스케줄 충돌 확인
// 현재 인원 수 확인
if (meeting.cur_num >= meeting.max_num) {
throw new Error("모임 인원이 모두 찼습니다.");
}
const hasConflict = await ScheduleService.checkScheduleOverlapByTime( const hasConflict = await ScheduleService.checkScheduleOverlapByTime(
userId, userId,
meeting.time_idx_start, meeting.time_idx_start,
...@@ -217,7 +348,7 @@ class MeetingService { ...@@ -217,7 +348,7 @@ class MeetingService {
); );
console.log(`스케줄 충돌 결과: ${hasConflict}`); console.log(`스케줄 충돌 결과: ${hasConflict}`);
if (hasConflict) { if (hasConflict) {
throw new Error('스케줄이 겹칩니다. 다른 모임에 참가하세요.'); throw new Error("스케줄이 겹칩니다. 다른 모임에 참가하세요.");
} }
// 참가자 추가 // 참가자 추가
...@@ -228,10 +359,13 @@ class MeetingService { ...@@ -228,10 +359,13 @@ class MeetingService {
// 스케줄 생성 (모임 시간 범위 내 모든 time_idx에 대해 생성) // 스케줄 생성 (모임 시간 범위 내 모든 time_idx에 대해 생성)
const events = []; const events = [];
for (let idx = meeting.time_idx_start; idx <= meeting.time_idx_end; idx++) { for (
let idx = meeting.time_idx_start;
idx <= meeting.time_idx_end;
idx++
) {
events.push({ time_idx: idx }); events.push({ time_idx: idx });
} }
await ScheduleService.createSchedules( await ScheduleService.createSchedules(
{ {
userId: userId, userId: userId,
...@@ -243,9 +377,13 @@ class MeetingService { ...@@ -243,9 +377,13 @@ class MeetingService {
); );
// 채팅방 참가 (MongoDB) // 채팅방 참가 (MongoDB)
const user = await User.findOne({ where: { id: userId }, transaction }); const user = await User.findOne({
const chatRoom = await ChatRooms.findOne({ chatRoomId: meeting.chatRoomId }); where: { id: userId },
transaction,
});
const chatRoom = await ChatRooms.findOne({
chatRoomId: meeting.chatRoomId,
});
if (chatRoom && !chatRoom.participants.includes(user.name)) { if (chatRoom && !chatRoom.participants.includes(user.name)) {
chatRoom.participants.push(user.name); chatRoom.participants.push(user.name);
chatRoom.isOnline.set(user.name, true); chatRoom.isOnline.set(user.name, true);
...@@ -253,15 +391,13 @@ class MeetingService { ...@@ -253,15 +391,13 @@ class MeetingService {
chatRoom.lastReadLogId.set(user.name, null); chatRoom.lastReadLogId.set(user.name, null);
await chatRoom.save(); 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) { async getMeetingDetail(meetingId) {
const meeting = await Meeting.findByPk(meetingId, { const meeting = await Meeting.findByPk(meetingId, {
include: [ include: [
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment