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

[#13] 스케쥴 서비스 코드 로직 대폭 수정 및 미팅서비스 테스트 완료

parents da135a2f 799bdae6
No related branches found
No related tags found
2 merge requests!31Develop,!23[#13] 스케쥴 서비스 코드 로직 대폭 수정 및 미팅서비스 테스트 완료
// src/config/sequelize.js // config/sequelize.js
const { Sequelize } = require('sequelize'); const { Sequelize } = require('sequelize');
require('dotenv').config(); require('dotenv').config();
const isTest = process.env.NODE_ENV === 'test'; const isTest = process.env.NODE_ENV === 'test';
const sequelize = isTest const sequelize = isTest
? new Sequelize('sqlite::memory:', { logging: false }) // 테스트 환경용 인메모리 DB ? new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
}) // 테스트 환경용 인메모리 DB
: new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASSWORD, { : new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASSWORD, {
host: process.env.DB_HOST, host: process.env.DB_HOST,
dialect: 'mysql', dialect: 'mysql',
......
// controllers/meetingController.js // controllers/meetingController.js
const MeetingService = require('../services/meetingService'); const MeetingService = require('../services/meetingService');
const CreateMeetingRequestDTO = require('../dtos/CreateMeetingRequestDTO'); const CreateMeetingRequestDTO = require('../dtos/CreateMeetingRequestDTO');
...@@ -7,13 +6,25 @@ class MeetingController { ...@@ -7,13 +6,25 @@ class MeetingController {
/** /**
* 번개 모임 생성 * 번개 모임 생성
* POST /api/meetings * POST /api/meetings
*
* 프론트엔드 입력 데이터 형식 예시:
* {
* "title": "팀 동기화 미팅",
* "description": "월간 팀 동기화 회의입니다.",
* "time_idx_start": 40, // 예: 10:00 AM
* "time_idx_end": 42, // 예: 10:30 AM
* "location": "회의실 A",
* "deadline": "2024-04-25T23:59:59Z",
* "type": "OPEN" // "OPEN" 또는 "CLOSE"
* }
*/ */
async createMeeting(req, res) { async createMeeting(req, res) {
try { try {
const userId = req.user.id; const userId = req.user.id;
const meetingData = { ...req.body, created_by: userId }; const meetingData = {
...req.body,
// CreateMeetingRequestDTO를 사용하여 요청 데이터 검증 created_by: userId
};
const createMeetingDTO = new CreateMeetingRequestDTO(meetingData); const createMeetingDTO = new CreateMeetingRequestDTO(meetingData);
createMeetingDTO.validate(); createMeetingDTO.validate();
...@@ -32,7 +43,6 @@ class MeetingController { ...@@ -32,7 +43,6 @@ class MeetingController {
async getMeetings(req, res) { async getMeetings(req, res) {
try { try {
const userId = req.user.id; // 인증 미들웨어를 통해 설정된 사용자 ID const userId = req.user.id; // 인증 미들웨어를 통해 설정된 사용자 ID
const meetings = await MeetingService.getMeetings(userId); const meetings = await MeetingService.getMeetings(userId);
res.status(200).json(meetings); res.status(200).json(meetings);
} catch (err) { } catch (err) {
...@@ -43,11 +53,10 @@ class MeetingController { ...@@ -43,11 +53,10 @@ class MeetingController {
/** /**
* 번개 모임 마감 * 번개 모임 마감
* PATCH /api/meetings/:meetingId/close * PATCH /api/meetings/:meetingId/close (URL 파라미터로 meetingId 전달)
*/ */
async closeMeeting(req, res) { async closeMeeting(req, res) {
const { meetingId } = req.params; const { meetingId } = req.params;
try { try {
const meeting = await MeetingService.closeMeeting(meetingId); const meeting = await MeetingService.closeMeeting(meetingId);
res.status(200).json({ message: '모임이 마감되었습니다.', meeting }); res.status(200).json({ message: '모임이 마감되었습니다.', meeting });
...@@ -60,6 +69,7 @@ class MeetingController { ...@@ -60,6 +69,7 @@ class MeetingController {
/** /**
* 번개 모임 참가 * 번개 모임 참가
* POST /api/meetings/:meetingId/join * POST /api/meetings/:meetingId/join
* (URL 파라미터로 meetingId 전달)
*/ */
async joinMeeting(req, res) { async joinMeeting(req, res) {
try { try {
...@@ -67,6 +77,7 @@ class MeetingController { ...@@ -67,6 +77,7 @@ class MeetingController {
const userId = req.user.id; // 인증 미들웨어를 통해 설정된 사용자 ID const userId = req.user.id; // 인증 미들웨어를 통해 설정된 사용자 ID
await MeetingService.joinMeeting(meetingId, userId); await MeetingService.joinMeeting(meetingId, userId);
res.status(200).json({ message: '모임 및 채팅방 참가 완료' }); res.status(200).json({ message: '모임 및 채팅방 참가 완료' });
} catch (err) { } catch (err) {
console.error('모임 참가 오류:', err); console.error('모임 참가 오류:', err);
...@@ -77,10 +88,10 @@ class MeetingController { ...@@ -77,10 +88,10 @@ class MeetingController {
/** /**
* 번개 모임 상세 조회 * 번개 모임 상세 조회
* GET /api/meetings/:meetingId * GET /api/meetings/:meetingId
(URL 파라미터로 meetingId 전달)
*/ */
async getMeetingDetail(req, res) { async getMeetingDetail(req, res) {
const { meetingId } = req.params; const { meetingId } = req.params;
try { try {
const meetingDetail = await MeetingService.getMeetingDetail(meetingId); const meetingDetail = await MeetingService.getMeetingDetail(meetingId);
res.status(200).json(meetingDetail); res.status(200).json(meetingDetail);
......
// dtos/CreateMeetingRequestDTO.js // dtos/CreateMeetingRequestDTO.js
const Joi = require('joi'); const Joi = require('joi');
class CreateMeetingRequestDTO { class CreateMeetingRequestDTO {
constructor({ title, description, start_time, end_time, location, deadline, type, created_by }) { constructor({ title, description, time_idx_start, time_idx_end, location, time_idx_deadline, type, created_by }) {
this.title = title; this.data = {
this.description = description; title,
this.start_time = start_time; description,
this.end_time = end_time; time_idx_start,
this.location = location; time_idx_end,
this.deadline = deadline; location,
this.type = type; time_idx_deadline,
this.created_by = created_by; type,
created_by,
};
} }
validate() { validate() {
const schema = Joi.object({ const schema = Joi.object({
title: Joi.string().min(1).max(255).required(), title: Joi.string().min(1).max(255).required(),
description: Joi.string().allow('', null).optional(), description: Joi.string().allow('', null).optional(),
start_time: Joi.date().iso().required(), time_idx_start: Joi.number().integer().min(0).required(),
end_time: Joi.date().iso().greater(Joi.ref('start_time')).required(), time_idx_end: Joi.number().integer().greater(Joi.ref('time_idx_start')).required(),
location: Joi.string().allow('', null).optional(), location: Joi.string().allow('', null).optional(),
deadline: Joi.date().iso().less(Joi.ref('start_time')).optional(), time_idx_deadline: Joi.number().integer().min(0).less(Joi.ref('time_idx_start')).optional(),
type: Joi.string().valid('OPEN', 'CLOSE').required(), type: Joi.string().valid('OPEN', 'CLOSE').required(),
created_by: Joi.number().integer().positive().required() created_by: Joi.number().integer().positive().required(),
}); });
const { error } = schema.validate(this, { abortEarly: false }); const { error } = schema.validate(this.data, { abortEarly: false });
if (error) { if (error) {
const errorMessages = error.details.map(detail => detail.message).join(', '); const errorMessages = error.details.map((detail) => detail.message).join(', ');
throw new Error(`Validation error: ${errorMessages}`); throw new Error(`Validation error: ${errorMessages}`);
} }
......
// dtos/MeetingDetailResponseDTO.js // dtos/MeetingResponseDTO.js
class MeetingDetailResponseDTO { class MeetingDetailResponseDTO {
constructor(meeting) { constructor(meeting) {
this.id = meeting.id; this.id = meeting.id;
this.title = meeting.title; this.title = meeting.title;
this.description = meeting.description; this.description = meeting.description;
this.startTime = meeting.start_time; this.timeIdxStart = meeting.time_idx_start;
this.endTime = meeting.end_time; this.timeIdxEnd = meeting.time_idx_end;
this.location = meeting.location; this.location = meeting.location;
this.deadline = meeting.deadline; this.time_idx_deadline = meeting.time_idx_deadline;
this.type = meeting.type; this.type = meeting.type;
this.creatorName = meeting.creator ? meeting.creator.name : 'Unknown'; this.creatorName = meeting.creator ? meeting.creator.name : 'Unknown';
this.participants = meeting.participants.map(participant => ({ this.participants = meeting.participants.map(participant => ({
......
// dtos/MeetingResponseDTO.js // dtos/MeetingDetailResponseDTO.js
class MeetingResponseDTO { class MeetingResponseDTO {
constructor(meeting, isParticipant, isScheduleConflict, creatorName) { constructor(meeting, isParticipant, isScheduleConflict, creatorName) {
this.id = meeting.id; this.id = meeting.id;
this.title = meeting.title; this.title = meeting.title;
this.description = meeting.description; this.description = meeting.description;
this.startTime = meeting.start_time; this.timeIdxStart = meeting.time_idx_start;
this.endTime = meeting.end_time; this.timeIdxEnd = meeting.time_idx_end;
this.location = meeting.location; this.location = meeting.location;
this.deadline = meeting.deadline; this.time_idx_deadline = meeting.time_idx_deadline;
this.type = meeting.type; this.type = meeting.type;
this.creatorName = creatorName; this.creatorName = creatorName;
this.isParticipant = isParticipant; this.isParticipant = isParticipant;
......
// middlewares/auth.js // middlewares/auth.js
exports.isLoggedIn = (req, res, next) => { exports.isLoggedIn = (req, res, next) => { //로그인된 사용자자만 접근허용
if (req.isAuthenticated()) { if (req.isAuthenticated()) {
return next(); return next();
} }
res.redirect('/auth/login'); res.redirect('/auth/login');
}; };
exports.isNotLoggedIn = (req, res, next) => { exports.isNotLoggedIn = (req, res, next) => { //로그인 안되면 리다이렉트
if (!req.isAuthenticated()) { if (!req.isAuthenticated()) {
return next(); return next();
} }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
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 Friend = sequelize.define('Friend', { const Friend = sequelize.define('Friend', {
status: { status: {
...@@ -25,11 +25,4 @@ const Friend = sequelize.define('Friend', { ...@@ -25,11 +25,4 @@ const Friend = sequelize.define('Friend', {
] ]
}); });
// // 관계 설정
// Friend.belongsTo(User, { foreignKey: 'requester_id', as: 'requester' }); // 친구 요청을 보낸 사용자
// Friend.belongsTo(User, { foreignKey: 'receiver_id', as: 'receiver' }); // 친구 요청을 받은 사용자
// User.hasMany(Friend, { foreignKey: 'requester_id', as: 'sentRequests' }); // 친구 요청을 보낸 목록
// User.hasMany(Friend, { foreignKey: 'receiver_id', as: 'receivedRequests' }); // 친구 요청을 받은 목록
module.exports = Friend; module.exports = Friend;
// models/index.js // models/index.js
const sequelize = require('../config/sequelize'); const sequelize = require('../config/sequelize');
const User = require('./User'); const User = require('./user');
const Friend = require('./Friend'); const Friend = require('./Friend');
const Schedule = require('./Schedule'); const Schedule = require('./Schedule');
const Meeting = require('./Meeting'); const Meeting = require('./Meeting');
const MeetingParticipant = require('./MeetingParticipant'); const MeetingParticipant = require('./MeetingParticipant');
const ChatRoom = require('./ChatRooms'); const ChatRooms = require('./ChatRooms');
// 관계 설정 // 관계 설정
Friend.belongsTo(User, { foreignKey: 'requester_id', as: 'requester' }); // 친구 요청을 보낸 사용자 Friend.belongsTo(User, { foreignKey: 'requester_id', as: 'requester' }); // 친구 요청을 보낸 사용자
...@@ -34,5 +34,5 @@ module.exports = { ...@@ -34,5 +34,5 @@ module.exports = {
Schedule, Schedule,
Meeting, Meeting,
MeetingParticipant, MeetingParticipant,
ChatRoom, ChatRooms,
}; };
// 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: {
...@@ -12,41 +11,27 @@ const Meeting = sequelize.define('Meeting', { ...@@ -12,41 +11,27 @@ const Meeting = sequelize.define('Meeting', {
description: { description: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
}, },
start_time: { time_idx_start: {
type: DataTypes.DATE, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
}, },
end_time: { time_idx_end: {
type: DataTypes.DATE, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
}, },
location: { location: {
type: DataTypes.STRING, type: DataTypes.STRING,
}, },
deadline: { time_idx_deadline: {
type: DataTypes.DATE, type: DataTypes.INTEGER,
}, },
type: { type: {
type: DataTypes.ENUM('OPEN', 'CLOSE'), type: DataTypes.ENUM('OPEN', 'CLOSE'),
allowNull: false, allowNull: false,
}, },
created_by: {
type: DataTypes.INTEGER,
allowNull: false,
},
chatRoomId: { // 새로운 필드 추가
type: DataTypes.STRING,
allowNull: false,
},
}, { }, {
tableName: 'Meetings', tableName: 'Meetings',
timestamps: false, timestamps: false,
}); });
// // 연관 관계 설정
// Meeting.belongsTo(User, { foreignKey: 'created_by', as: 'creator' });
// User.hasMany(Meeting, { foreignKey: 'created_by', as: 'meetings' });
module.exports = Meeting; module.exports = Meeting;
// models/MeetingParticipant.js
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
const sequelize = require('../config/sequelize'); const sequelize = require('../config/sequelize');
const Meeting =require('./Meeting');
const User = require('./User');
const MeetingParticipant = sequelize.define('MeetingParticipant', { const MeetingParticipant = sequelize.define(
'MeetingParticipant',
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
meeting_id: { meeting_id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false allowNull: false,
}, },
user_id: { user_id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false allowNull: false,
},
},
{
tableName: 'MeetingParticipants', // 테이블 이름 설정
timestamps: false, // createdAt, updatedAt 필드 비활성화
} }
}, { );
tableName: 'MeetingParticipants',
timestamps: false,
});
// MeetingParticipant.belongsTo(Meeting, { foreignKey: 'meeting_id', as: 'meeting' });
// Meeting.hasMany(MeetingParticipant, { foreignKey: 'meeting_id', as: 'participants' });
// MeetingParticipant.belongsTo(User, { foreignKey: 'user_id', as: 'user' });
// User.hasMany(MeetingParticipant, { foreignKey: 'user_id', as: 'meetingParticipations' });
module.exports = MeetingParticipant; module.exports = MeetingParticipant;
// models/Schedule.js // models/Schedule.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 Schedule = sequelize.define('Schedule', { const Schedule = sequelize.define('Schedule', {
title: { title: {
......
This diff is collapsed.
...@@ -18,9 +18,7 @@ ...@@ -18,9 +18,7 @@
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.21.1", "express": "^4.21.1",
"express-session": "^1.18.1", "express-session": "^1.18.1",
"jest": "^29.7.0",
"joi": "^17.13.3", "joi": "^17.13.3",
"mongoose": "^8.8.1",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql2": "^3.11.4", "mysql2": "^3.11.4",
...@@ -34,6 +32,10 @@ ...@@ -34,6 +32,10 @@
"devDependencies": { "devDependencies": {
"artillery": "^2.0.21", "artillery": "^2.0.21",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"nodemon": "^3.1.7" "jest": "^29.7.0",
"jest-mock": "^29.7.0",
"mongoose": "^8.8.2",
"nodemon": "^3.1.7",
"sequelize-mock": "^0.10.2"
} }
} }
// services/meetingService.js // services/meetingService.js
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
const { Op } = require('sequelize'); const { Op } = require('sequelize');
const { Meeting, MeetingParticipant, User, Schedule, ChatRoom } = require('../models'); // models/index.js를 통해 임포트 const sequelize = require('../config/sequelize'); // 트랜잭션 관리를 위해 sequelize 인스턴스 필요
const chatController = require('../controllers/chatController'); const { Meeting, MeetingParticipant, User, Schedule } = require('../models');
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');
const ScheduleService = require('./scheduleService'); // ScheduleService 임포트 const ScheduleService = require('./scheduleService');
class MeetingService { class MeetingService {
/**
* 현재 시간을 time_idx로 변환하는 유틸리티 함수
* 월요일부터 일요일까지 15분 단위로 타임 인덱스를 할당
* 현재 시간의 타임 인덱스 (0 ~ 671)
*/
getCurrentTimeIdx() {
const today = new Date();
const jsDayOfWeek = today.getDay(); // 0=Sunday, 1=Monday, ..., 6=Saturday
const adjustedDayOfWeek = (jsDayOfWeek + 6) % 7; // 0=Monday, ..., 6=Sunday
const hours = today.getHours();
const minutes = today.getMinutes();
const timeIdx = hours * 4 + Math.floor(minutes / 15); // 15분 단위 인덱스
const totalIdx = adjustedDayOfWeek * 96 + timeIdx; // 주 전체 인덱스
return totalIdx;
}
/** /**
* 번개 모임 생성 * 번개 모임 생성
* @param {object} meetingData - 모임 생성 데이터 * @param {object} meetingData - 모임 생성 데이터
...@@ -20,7 +37,16 @@ class MeetingService { ...@@ -20,7 +37,16 @@ class MeetingService {
const createMeetingDTO = new CreateMeetingRequestDTO(meetingData); const createMeetingDTO = new CreateMeetingRequestDTO(meetingData);
createMeetingDTO.validate(); createMeetingDTO.validate();
const { title, description, start_time, end_time, location, deadline, type, created_by } = meetingData; const {
title,
description,
time_idx_start,
time_idx_end,
location,
time_idx_deadline,
type,
created_by,
} = meetingData;
// 사용자 존재 여부 확인 // 사용자 존재 여부 확인
const user = await User.findOne({ where: { id: created_by } }); const user = await User.findOne({ where: { id: created_by } });
...@@ -28,57 +54,61 @@ class MeetingService { ...@@ -28,57 +54,61 @@ class MeetingService {
throw new Error('사용자를 찾을 수 없습니다.'); throw new Error('사용자를 찾을 수 없습니다.');
} }
// 스케줄 충돌 확인
const hasConflict = await ScheduleService.checkScheduleOverlap(
created_by,
new Date(start_time),
new Date(end_time)
);
if (hasConflict) {
throw new Error('스케줄이 겹칩니다. 다른 시간을 선택해주세요.');
}
// 트랜잭션을 사용하여 모임 생성과 스케줄 추가를 원자적으로 처리 // 트랜잭션을 사용하여 모임 생성과 스케줄 추가를 원자적으로 처리
const result = await ScheduleService.withTransaction(async (transaction) => { const result = await sequelize.transaction(async (transaction) => {
// 채팅방 생성 // 채팅방 생성 (MongoDB)
const chatRoomId = uuidv4(); // 고유한 채팅방 ID 생성
const chatRoomData = { const chatRoomData = {
chatRoomId,
participants: [user.name], participants: [user.name],
messages: [],
lastReadAt: {},
lastReadLogId: {},
isOnline: {},
}; };
const chatRoomResponse = await chatController.createChatRoomInternal(chatRoomData); const chatRoom = new ChatRooms(chatRoomData);
await chatRoom.save();
if (!chatRoomResponse.success) {
throw new Error('채팅방 생성 실패');
}
const chatRoomId = chatRoomResponse.chatRoomId;
// 모임 생성 // 모임 생성
const newMeeting = await Meeting.create({ const newMeeting = await Meeting.create(
{
title, title,
description, description,
start_time, time_idx_start,
end_time, time_idx_end,
location, location,
deadline, time_idx_deadline,
type, type,
created_by, created_by,
chatRoomId, chatRoomId,
}, { transaction }); },
{ transaction }
);
// 모임 참가자 추가 (생성자 자신) // 모임 참가자 추가 (생성자 자신)
await MeetingParticipant.create({ await MeetingParticipant.create(
{
meeting_id: newMeeting.id, meeting_id: newMeeting.id,
user_id: created_by, user_id: created_by,
}, { transaction }); },
{ transaction }
);
// 스케줄 생성 (모임 시간 범위 내 모든 time_idx에 대해 생성)
const events = [];
for (let idx = time_idx_start; idx <= time_idx_end; idx++) {
events.push({ time_idx: idx });
}
// 스케줄 추가 (트랜잭션 전달) await ScheduleService.createSchedules(
await ScheduleService.createSchedule({ {
userId: created_by, userId: created_by,
title: `번개 모임: ${title}`, title: `번개 모임: ${title}`,
start_time: new Date(start_time), is_fixed: false,
end_time: new Date(end_time), events: events,
is_fixed: true, },
}, transaction); transaction
);
return { meeting_id: newMeeting.id, chatRoomId }; return { meeting_id: newMeeting.id, chatRoomId };
}); });
...@@ -93,31 +123,34 @@ class MeetingService { ...@@ -93,31 +123,34 @@ class MeetingService {
*/ */
async getMeetings(userId) { async getMeetings(userId) {
const meetings = await Meeting.findAll({ const meetings = await Meeting.findAll({
attributes: ['id', 'title', 'description', 'start_time', 'end_time', 'location', 'deadline', 'type'], attributes: [
'id',
'title',
'description',
'time_idx_start',
'time_idx_end',
'location',
'time_idx_deadline',
'type',
],
include: [ include: [
{
model: User,
as: 'creator',
attributes: ['name'],
},
{ {
model: MeetingParticipant, model: MeetingParticipant,
as: 'participants', as: 'participants',
attributes: ['user_id'], where: { user_id: userId }, // userId와 매핑된 미팅만 가져옴
attributes: [], // MeetingParticipant 테이블의 데이터는 필요 없으므로 제외
},
{
model: User,
as: 'creator',
attributes: ['name'], // 미팅 생성자의 이름만 필요
}, },
], ],
}); });
return meetings.map((meeting) => { return meetings.map((meeting) => {
const creatorName = meeting.creator ? meeting.creator.name : 'Unknown'; const creatorName = meeting.creator ? meeting.creator.name : 'Unknown';
const isParticipant = meeting.participants.some(participant => participant.user_id === parseInt(userId, 10)); return new MeetingResponseDTO(meeting, true, false, creatorName);
return new MeetingResponseDTO(
meeting,
isParticipant,
false, // isScheduleConflict: 필요 시 추가 로직 구현
creatorName
);
}); });
} }
...@@ -149,6 +182,7 @@ class MeetingService { ...@@ -149,6 +182,7 @@ class MeetingService {
*/ */
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)}`);
if (!meeting) { if (!meeting) {
throw new Error('모임을 찾을 수 없습니다.'); throw new Error('모임을 찾을 수 없습니다.');
} }
...@@ -157,12 +191,15 @@ class MeetingService { ...@@ -157,12 +191,15 @@ class MeetingService {
throw new Error('이미 마감된 모임입니다.'); throw new Error('이미 마감된 모임입니다.');
} }
if (new Date() > new Date(meeting.deadline)) { if (meeting.time_idx_deadline !== undefined) {
const currentTimeIdx = this.getCurrentTimeIdx(); // 현재 시간 인덱스
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) {
...@@ -170,39 +207,51 @@ class MeetingService { ...@@ -170,39 +207,51 @@ class MeetingService {
} }
// 트랜잭션을 사용하여 참가자 추가 및 스케줄 업데이트를 원자적으로 처리 // 트랜잭션을 사용하여 참가자 추가 및 스케줄 업데이트를 원자적으로 처리
await ScheduleService.withTransaction(async (transaction) => { await sequelize.transaction(async (transaction) => {
// 참가자 추가
await MeetingParticipant.create({ meeting_id: meetingId, user_id: userId }, { transaction });
// 스케줄 충돌 확인 // 스케줄 충돌 확인
const hasConflict = await ScheduleService.checkScheduleOverlap( const hasConflict = await ScheduleService.checkScheduleOverlapByTime(
userId, userId,
new Date(meeting.start_time), meeting.time_idx_start,
new Date(meeting.end_time) meeting.time_idx_end,
transaction
); );
console.log(`스케줄 충돌 결과: ${hasConflict}`);
if (hasConflict) { if (hasConflict) {
throw new Error('스케줄이 겹칩니다. 다른 모임에 참가하세요.'); throw new Error('스케줄이 겹칩니다. 다른 모임에 참가하세요.');
} }
// 스케줄 추가 (트랜잭션 전달) // 참가자 추가
await ScheduleService.createSchedule({ 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, userId: userId,
title: `번개 모임: ${meeting.title}`, title: `번개 모임: ${meeting.title}`,
start_time: new Date(meeting.start_time),
end_time: new Date(meeting.end_time),
is_fixed: true, is_fixed: true,
}, transaction); events: events,
},
transaction
);
// 채팅방 참가 // 채팅방 참가 (MongoDB)
const user = await User.findOne({ where: { id: userId }, transaction }); const user = await User.findOne({ where: { id: userId }, transaction });
const chatRoom = await ChatRoom.findOne({ where: { meeting_id: meetingId }, 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);
chatRoom.lastReadAt.set(user.name, new Date()); chatRoom.lastReadAt.set(user.name, new Date());
chatRoom.lastReadLogId.set(user.name, null); chatRoom.lastReadLogId.set(user.name, null);
await chatRoom.save({ transaction }); await chatRoom.save();
} }
}); });
} }
...@@ -212,30 +261,31 @@ class MeetingService { ...@@ -212,30 +261,31 @@ class MeetingService {
* @param {number} meetingId - 모임 ID * @param {number} meetingId - 모임 ID
* @returns {Promise<MeetingDetailResponseDTO>} - 모임 상세 DTO * @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: [
{ {
model: User, model: User,
as: 'creator', as: "creator",
attributes: ['name'] attributes: ["name"],
}, },
{ {
model: MeetingParticipant, model: MeetingParticipant,
as: 'participants', as: "participants",
include: [ include: [
{ {
model: User, model: User,
as: 'participantUser', as: "user", // 'participantUser'에서 'user'로 수정
attributes: ['name', 'email'] attributes: ["name", "email"],
} },
] ],
} },
] ],
}); });
if (!meeting) { if (!meeting) {
throw new Error('모임을 찾을 수 없습니다.'); throw new Error("모임을 찾을 수 없습니다.");
} }
return new MeetingDetailResponseDTO(meeting); return new MeetingDetailResponseDTO(meeting);
......
This diff is collapsed.
...@@ -32,7 +32,6 @@ beforeEach(async () => { ...@@ -32,7 +32,6 @@ beforeEach(async () => {
}); });
afterAll(async () => { afterAll(async () => {
// 모든 테스트가 끝난 후 데이터베이스 연결을 종료합니다.
await sequelize.close(); await sequelize.close();
}); });
...@@ -272,7 +271,7 @@ describe('ScheduleService', () => { ...@@ -272,7 +271,7 @@ describe('ScheduleService', () => {
}); });
test('should retrieve one schedule when user has only one', async () => { test('should retrieve one schedule when user has only one', async () => {
const schedules = await ScheduleService.getAllSchedules(3); // Charlie has id=3 and only one fixed schedule const schedules = await ScheduleService.getAllSchedules(3);
expect(schedules).toBeDefined(); expect(schedules).toBeDefined();
expect(Array.isArray(schedules)).toBe(true); expect(Array.isArray(schedules)).toBe(true);
...@@ -309,7 +308,7 @@ describe('ScheduleService', () => { ...@@ -309,7 +308,7 @@ describe('ScheduleService', () => {
describe('cleanExpiredSchedules', () => { describe('cleanExpiredSchedules', () => {
test('should delete all flexible schedules', async () => { test('should delete all flexible schedules', async () => {
// 먼저, 여러 유동 스케줄을 생성 // 여러 유동 스케줄을 생성
await ScheduleService.createSchedules({ await ScheduleService.createSchedules({
userId: 1, userId: 1,
title: 'Alice Flexible Schedule 2', title: 'Alice Flexible Schedule 2',
...@@ -342,7 +341,7 @@ describe('ScheduleService', () => { ...@@ -342,7 +341,7 @@ describe('ScheduleService', () => {
}); });
test('should not delete fixed schedules', async () => { test('should not delete fixed schedules', async () => {
// 먼저, 여러 고정 스케줄을 생성 // 여러 고정 스케줄을 생성
await ScheduleService.createSchedules({ await ScheduleService.createSchedules({
userId: 3, userId: 3,
title: 'Charlie Fixed Schedule 2', title: 'Charlie Fixed Schedule 2',
...@@ -361,7 +360,7 @@ describe('ScheduleService', () => { ...@@ -361,7 +360,7 @@ describe('ScheduleService', () => {
where: { user_id: 3, is_fixed: true }, where: { user_id: 3, is_fixed: true },
}); });
expect(remainingFixedSchedules.length).toBe(3); // 기존 1개 + 2개 추가 expect(remainingFixedSchedules.length).toBe(3);
}); });
}); });
}); });
...@@ -7,21 +7,16 @@ const ScheduleResponseDTO = require('../dtos/ScheduleResponseDTO'); ...@@ -7,21 +7,16 @@ const ScheduleResponseDTO = require('../dtos/ScheduleResponseDTO');
class ScheduleService { class ScheduleService {
/** /**
* 스케줄 생성 (벌크) * 스케줄 생성 (벌크)
* @param {object} [transaction] - Sequelize 트랜잭션 객체 -> 미팅방에서 쓰기위해 트랜잭션을 넘겨받는걸 추가
*/ */
async createSchedules({ userId, title, is_fixed, events }) { async createSchedules({ userId, title, is_fixed, events }, transaction = null) {
return await sequelize.transaction(async (transaction) => {
const scheduleDTOs = []; const scheduleDTOs = [];
for (const event of events) { for (const event of events) {
const { time_idx } = event; const { time_idx } = event;
// 중복 스케줄 검사 // 중복 스케줄 검사
const overlap = await this.checkScheduleOverlap( const overlap = await this.checkScheduleOverlap(userId, time_idx, transaction);
userId,
time_idx,
transaction
);
if (overlap) { if (overlap) {
throw new Error(`Schedule overlaps with existing schedule at time_idx ${time_idx}`); throw new Error(`Schedule overlaps with existing schedule at time_idx ${time_idx}`);
} }
...@@ -38,14 +33,13 @@ class ScheduleService { ...@@ -38,14 +33,13 @@ class ScheduleService {
} }
return scheduleDTOs; return scheduleDTOs;
});
} }
/** /**
* 스케줄 수정 (벌크) * 스케줄 수정 (벌크)
* @param {Array} updates - 수정할 스케줄 배열
*/ */
async updateSchedules(userId, updates) { async updateSchedules(userId, updates, transaction = null) {
return await sequelize.transaction(async (transaction) => {
const updatedSchedules = []; const updatedSchedules = [];
for (const update of updates) { for (const update of updates) {
...@@ -57,10 +51,9 @@ class ScheduleService { ...@@ -57,10 +51,9 @@ class ScheduleService {
}); });
if (!schedule) { if (!schedule) {
throw { code: 'SCHEDULE_NOT_FOUND', message: `Schedule not found at time_idx ${time_idx}` }; throw new Error(`Schedule not found at time_idx ${time_idx}`);
} }
const updatedData = {}; const updatedData = {};
if (title !== undefined) updatedData.title = title; if (title !== undefined) updatedData.title = title;
if (is_fixed !== undefined) updatedData.is_fixed = is_fixed; if (is_fixed !== undefined) updatedData.is_fixed = is_fixed;
...@@ -70,14 +63,15 @@ class ScheduleService { ...@@ -70,14 +63,15 @@ class ScheduleService {
} }
return updatedSchedules; return updatedSchedules;
});
} }
/** /**
* 스케줄 삭제 (벌크) * 스케줄 삭제 (벌크)
* @param {number} userId - 사용자 ID
* @param {Array<number>} time_idxs - 삭제할 스케줄의 time_idx 배열
* @param {object} [transaction] - Sequelize 트랜잭션 객체
*/ */
async deleteSchedules(userId, time_idxs) { async deleteSchedules(userId, time_idxs, transaction = null) {
return await sequelize.transaction(async (transaction) => {
const deleted_time_idxs = []; const deleted_time_idxs = [];
for (const time_idx of time_idxs) { for (const time_idx of time_idxs) {
...@@ -94,7 +88,6 @@ class ScheduleService { ...@@ -94,7 +88,6 @@ class ScheduleService {
} }
return { deleted_time_idxs }; return { deleted_time_idxs };
});
} }
/** /**
...@@ -130,7 +123,7 @@ class ScheduleService { ...@@ -130,7 +123,7 @@ class ScheduleService {
/** /**
* 중복 스케줄 검사 * 중복 스케줄 검사
*/ */
async checkScheduleOverlap(userId, time_idx, transaction) { async checkScheduleOverlap(userId, time_idx, transaction = null) {
const overlappingSchedule = await Schedule.findOne({ const overlappingSchedule = await Schedule.findOne({
where: { user_id: userId, time_idx }, where: { user_id: userId, time_idx },
transaction, transaction,
...@@ -139,6 +132,29 @@ class ScheduleService { ...@@ -139,6 +132,29 @@ class ScheduleService {
return !!overlappingSchedule; return !!overlappingSchedule;
} }
async checkScheduleOverlapByTime(userId, time_idx_start, time_idx_end, transaction = null) {
console.log(
`checkScheduleOverlapByTime 호출: userId=${userId}, time_idx_start=${time_idx_start}, time_idx_end=${time_idx_end}`
);
const overlappingSchedule = await Schedule.findOne({
where: {
user_id: userId,
time_idx: {
[Op.between]: [time_idx_start, time_idx_end]
}
},
transaction,
});
console.log(`중복 스케줄: ${JSON.stringify(overlappingSchedule)}`);
const result = !!overlappingSchedule;
console.log(`스케줄 충돌 결과: ${result}`);
return result;
}
/**
* 만료된 스케줄 삭제
*/
async cleanExpiredSchedules() { async cleanExpiredSchedules() {
try { try {
const deletedCount = await Schedule.destroy({ const deletedCount = await Schedule.destroy({
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment