From b3bea64d3609fc85660b16e01e5e59d8d10e8ca7 Mon Sep 17 00:00:00 2001 From: tpgus2603 <kakaneymar2424@gmail.com> Date: Thu, 21 Nov 2024 01:31:13 +0900 Subject: [PATCH] =?UTF-8?q?refactor,feature:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EC=A0=81=EC=9A=A9=20=EB=A1=9C=EC=A7=81=20=EB=B0=8F?= =?UTF-8?q?=20=EA=B2=80=EC=A6=9D=EB=A1=9C=EC=A7=81=20=EB=B3=B4=EA=B0=95(#1?= =?UTF-8?q?2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controllers/meetingController.js | 124 ++++++----- dtos/CreateMeetingRequestDTO.js | 39 ++++ dtos/MeetingDetailResponseDTO.js | 34 ++-- dtos/MeetingResponseDTO.js | 28 +-- services/meetingService.js | 339 +++++++++++++++++++------------ 5 files changed, 355 insertions(+), 209 deletions(-) create mode 100644 dtos/CreateMeetingRequestDTO.js diff --git a/controllers/meetingController.js b/controllers/meetingController.js index 4860322..182c25b 100644 --- a/controllers/meetingController.js +++ b/controllers/meetingController.js @@ -1,68 +1,94 @@ +// controllers/meetingController.js + const MeetingService = require('../services/meetingService'); +const CreateMeetingRequestDTO = require('../dtos/CreateMeetingRequestDTO'); class MeetingController { - async createMeeting(req, res) { - try { - const result = await MeetingService.createMeeting(req.body); - res.status(201).json(result); - } catch (err) { - console.error('번개 모임 생성 오류:', err); - res.status(500).json({ error: err.message || '번개 모임 생성 실패' }); - } - } + /** + * 번개 모임 생성 + * POST /api/meetings + */ + async createMeeting(req, res) { + try { + const userId = req.userId; // 인증 미들웨어를 통해 설정된 사용자 ID + const meetingData = { ...req.body, created_by: userId }; - async getMeetings(req, res) { - const { userId } = req.query; + // CreateMeetingRequestDTO를 사용하여 요청 데이터 검증 + const createMeetingDTO = new CreateMeetingRequestDTO(meetingData); + createMeetingDTO.validate(); - if (!userId) { - return res.status(400).json({ error: '사용자 ID가 필요합니다.' }); + const result = await MeetingService.createMeeting(meetingData); + res.status(201).json(result); + } catch (err) { + console.error('번개 모임 생성 오류:', err); + res.status(500).json({ error: err.message || '번개 모임 생성 실패' }); + } } - try { - const meetings = await MeetingService.getMeetings(userId); - res.status(200).json(meetings); - } catch (err) { - console.error('모임 목록 조회 오류:', err); - res.status(500).json({ error: err.message || '모임 목록 조회 실패' }); + /** + * 번개 모임 목록 조회 + * GET /api/meetings + */ + async getMeetings(req, res) { + try { + const userId = req.userId; // 인증 미들웨어를 통해 설정된 사용자 ID + + const meetings = await MeetingService.getMeetings(userId); + res.status(200).json(meetings); + } catch (err) { + console.error('모임 목록 조회 오류:', err); + res.status(500).json({ error: err.message || '모임 목록 조회 실패' }); + } } - } - async closeMeeting(req, res) { - const { meetingId } = req.params; + /** + * 번개 모임 마감 + * PATCH /api/meetings/:meetingId/close + */ + async closeMeeting(req, res) { + const { meetingId } = req.params; - try { - const meeting = await MeetingService.closeMeeting(meetingId); - res.status(200).json({ message: '모임이 마감되었습니다.', meeting }); - } catch (err) { - console.error('모임 마감 오류:', err); - res.status(500).json({ error: err.message || '모임 마감 실패' }); + try { + const meeting = await MeetingService.closeMeeting(meetingId); + res.status(200).json({ message: '모임이 마감되었습니다.', meeting }); + } catch (err) { + console.error('모임 마감 오류:', err); + res.status(500).json({ error: err.message || '모임 마감 실패' }); + } } - } - async joinMeeting(req, res) { - const { meetingId } = req.params; - const { user_id } = req.body; + /** + * 번개 모임 참가 + * POST /api/meetings/:meetingId/join + */ + async joinMeeting(req, res) { + try { + const { meetingId } = req.params; + const userId = req.userId; // 인증 미들웨어를 통해 설정된 사용자 ID - try { - await MeetingService.joinMeeting(meetingId, user_id); - res.status(200).json({ message: '모임 및 채팅방 참가 완료' }); - } catch (err) { - console.error('모임 참가 오류:', err); - res.status(500).json({ error: err.message || '모임 참가 실패' }); + await MeetingService.joinMeeting(meetingId, userId); + res.status(200).json({ message: '모임 및 채팅방 참가 완료' }); + } catch (err) { + console.error('모임 참가 오류:', err); + res.status(500).json({ error: err.message || '모임 참가 실패' }); + } } - } - async getMeetingDetail(req, res) { - const { meetingId } = req.params; + /** + * 번개 모임 상세 조회 + * GET /api/meetings/:meetingId + */ + async getMeetingDetail(req, res) { + const { meetingId } = req.params; - try { - const meetingDetail = await MeetingService.getMeetingDetail(meetingId); - res.status(200).json(meetingDetail); - } catch (err) { - console.error('모임 상세 조회 오류:', err); - res.status(500).json({ error: err.message || '모임 상세 조회 실패' }); + try { + const meetingDetail = await MeetingService.getMeetingDetail(meetingId); + res.status(200).json(meetingDetail); + } catch (err) { + console.error('모임 상세 조회 오류:', err); + res.status(500).json({ error: err.message || '모임 상세 조회 실패' }); + } } - } } -module.exports = new MeetingController(); \ No newline at end of file +module.exports = new MeetingController(); diff --git a/dtos/CreateMeetingRequestDTO.js b/dtos/CreateMeetingRequestDTO.js new file mode 100644 index 0000000..fa3801d --- /dev/null +++ b/dtos/CreateMeetingRequestDTO.js @@ -0,0 +1,39 @@ +// dtos/CreateMeetingRequestDTO.js + +const Joi = require('joi'); + +class CreateMeetingRequestDTO { + constructor({ title, description, start_time, end_time, location, deadline, type, created_by }) { + this.title = title; + this.description = description; + this.start_time = start_time; + this.end_time = end_time; + this.location = location; + this.deadline = deadline; + this.type = type; + this.created_by = created_by; + } + validate() { + const schema = Joi.object({ + title: Joi.string().min(1).max(255).required(), + description: Joi.string().allow('', null).optional(), + start_time: Joi.date().iso().required(), + end_time: Joi.date().iso().greater(Joi.ref('start_time')).required(), + location: Joi.string().allow('', null).optional(), + deadline: Joi.date().iso().greater(Joi.ref('start_time')).optional(), + type: Joi.string().valid('OPEN', 'CLOSE').required(), + created_by: Joi.number().integer().positive().required() + }); + + const { error } = schema.validate(this, { abortEarly: false }); + + if (error) { + const errorMessages = error.details.map(detail => detail.message).join(', '); + throw new Error(`Validation error: ${errorMessages}`); + } + + return true; + } +} + +module.exports = CreateMeetingRequestDTO; diff --git a/dtos/MeetingDetailResponseDTO.js b/dtos/MeetingDetailResponseDTO.js index 6670fe4..33a5582 100644 --- a/dtos/MeetingDetailResponseDTO.js +++ b/dtos/MeetingDetailResponseDTO.js @@ -1,20 +1,22 @@ -class MeetingDetailResponse { +// dtos/MeetingDetailResponseDTO.js + +class MeetingDetailResponseDTO { constructor(meeting) { - this.id = meeting.id; - this.title = meeting.title; - this.description = meeting.description; - this.startTime = meeting.start_time; - this.endTime = meeting.end_time; - this.location = meeting.location; - this.deadline = meeting.deadline; - this.type = meeting.type; - this.creatorName = meeting.creator ? meeting.creator.name : 'Unknown'; - this.participants = meeting.participants.map(participant => ({ - userId: participant.user_id, - name: participant.participantUser ? participant.participantUser.name : 'Unknown', - email: participant.participantUser ? participant.participantUser.email : 'Unknown' - })); + this.id = meeting.id; + this.title = meeting.title; + this.description = meeting.description; + this.startTime = meeting.start_time; + this.endTime = meeting.end_time; + this.location = meeting.location; + this.deadline = meeting.deadline; + this.type = meeting.type; + this.creatorName = meeting.creator ? meeting.creator.name : 'Unknown'; + this.participants = meeting.participants.map(participant => ({ + userId: participant.user_id, + name: participant.participantUser ? participant.participantUser.name : 'Unknown', + email: participant.participantUser ? participant.participantUser.email : 'Unknown' + })); } } -module.exports = MeetingDetailResponse; \ No newline at end of file +module.exports = MeetingDetailResponseDTO; diff --git a/dtos/MeetingResponseDTO.js b/dtos/MeetingResponseDTO.js index 8ee8b83..a456d69 100644 --- a/dtos/MeetingResponseDTO.js +++ b/dtos/MeetingResponseDTO.js @@ -1,19 +1,19 @@ -// dtos/MeetingResponse.js +// dtos/MeetingResponseDTO.js -class MeetingResponse { +class MeetingResponseDTO { constructor(meeting, isParticipant, isScheduleConflict, creatorName) { - this.id = meeting.id; - this.title = meeting.title; - this.description = meeting.description; - this.startTime = meeting.start_time; - this.endTime = meeting.end_time; - this.location = meeting.location; - this.deadline = meeting.deadline; - this.type = meeting.type; - this.creatorName = creatorName; - this.isParticipant = isParticipant; - this.isScheduleConflict = isScheduleConflict; + this.id = meeting.id; + this.title = meeting.title; + this.description = meeting.description; + this.startTime = meeting.start_time; + this.endTime = meeting.end_time; + this.location = meeting.location; + this.deadline = meeting.deadline; + this.type = meeting.type; + this.creatorName = creatorName; + this.isParticipant = isParticipant; + this.isScheduleConflict = isScheduleConflict; } } -module.exports = MeetingResponse; \ No newline at end of file +module.exports = MeetingResponseDTO; diff --git a/services/meetingService.js b/services/meetingService.js index 0d3b7ba..c0b3f43 100644 --- a/services/meetingService.js +++ b/services/meetingService.js @@ -1,159 +1,238 @@ +// services/meetingService.js + const { v4: uuidv4 } = require('uuid'); -const { Meeting, MeetingParticipant, User } = require('../models'); +const { Op } = require('sequelize'); +const { Meeting, MeetingParticipant, User, Schedule } = require('../models'); const ChatRoom = require('../models/chatRooms'); const chatController = require('../controllers/chatController'); -const MeetingResponse = require('../dtos/MeetingResponse'); +const MeetingResponseDTO = require('../dtos/MeetingResponseDTO'); const MeetingDetailResponseDTO = require('../dtos/MeetingDetailResponseDTO'); +const CreateMeetingRequestDTO = require('../dtos/CreateMeetingRequestDTO'); +const ScheduleService = require('./scheduleService'); // ScheduleService 임포트 class MeetingService { - async createMeeting(meetingData) { - const { title, description, start_time, end_time, location, deadline, type, created_by } = meetingData; + /** + * 번개 모임 생성 + * @returns 생성된 모임 ID와 채팅방 ID + */ + async createMeeting(meetingData) { + // DTO를 사용하여 요청 데이터 검증 + const createMeetingDTO = new CreateMeetingRequestDTO(meetingData); + createMeetingDTO.validate(); + + const { title, description, start_time, end_time, location, deadline, type, created_by } = meetingData; + + // 사용자 존재 여부 확인 + const user = await User.findOne({ where: { id: created_by } }); + if (!user) { + throw new Error('사용자를 찾을 수 없습니다.'); + } - const user = await User.findOne({ where: { id: created_by } }); - if (!user) { - throw new Error('사용자를 찾을 수 없습니다.'); - } + // 스케줄 충돌 확인 + const hasConflict = await ScheduleService.checkScheduleOverlap( + created_by, + new Date(start_time), + new Date(end_time) + ); + if (hasConflict) { + throw new Error('스케줄이 겹칩니다. 다른 시간을 선택해주세요.'); + } - const chatRoomData = { - participants: [user.name], - }; - const chatRoomResponse = await chatController.createChatRoomInternal(chatRoomData); + // 트랜잭션을 사용하여 모임 생성과 스케줄 추가를 원자적으로 처리 + const result = await Meeting.sequelize.transaction(async (transaction) => { + // 채팅방 생성 + const chatRoomData = { + participants: [user.name], + }; + const chatRoomResponse = await chatController.createChatRoomInternal(chatRoomData); - if (!chatRoomResponse.success) { - throw new Error('채팅방 생성 실패'); - } + if (!chatRoomResponse.success) { + throw new Error('채팅방 생성 실패'); + } - const chatRoomId = chatRoomResponse.chatRoomId; - - const newMeeting = await Meeting.create({ - title, - description, - start_time, - end_time, - location, - deadline, - type, - created_by, - chatRoomId, - }); - - await MeetingParticipant.create({ - meeting_id: newMeeting.id, - user_id: created_by, - }); - - return { meeting_id: newMeeting.id, chatRoomId: chatRoomResponse.chatRoomId }; - } - - async getMeetings(userId) { - const meetings = await Meeting.findAll({ - attributes: ['id', 'title', 'description', 'start_time', 'end_time', 'location', 'deadline', 'type'], - include: [ - { - model: User, - as: 'creator', - attributes: ['name'], - }, - { - model: MeetingParticipant, - as: 'participants', - attributes: ['user_id'], - }, - ], - }); - - return meetings.map((meeting) => { - const creatorName = meeting.creator ? meeting.creator.name : 'Unknown'; - const isParticipant = meeting.participants.some(participant => participant.user_id === parseInt(userId, 10)); - - return new MeetingResponse( - meeting, - isParticipant, - false, - creatorName - ); - }); - } - - async closeMeeting(meetingId) { - const meeting = await Meeting.findByPk(meetingId); - if (!meeting) { - throw new Error('모임을 찾을 수 없습니다.'); + const chatRoomId = chatRoomResponse.chatRoomId; + + // 모임 생성 + const newMeeting = await Meeting.create({ + title, + description, + start_time, + end_time, + location, + deadline, + type, + created_by, + chatRoomId, + }, { transaction }); + + // 모임 참가자 추가 (생성자 자신) + await MeetingParticipant.create({ + meeting_id: newMeeting.id, + user_id: created_by, + }, { transaction }); + + // 스케줄 추가 + await ScheduleService.createSchedule({ + userId: created_by, + title: `번개 모임: ${title}`, + start_time: new Date(start_time), + end_time: new Date(end_time), + is_fixed: true, + }); + + return { meeting_id: newMeeting.id, chatRoomId }; + }); + + return result; } - if (meeting.type === 'CLOSE') { - throw new Error('이미 마감된 모임입니다.'); + /** + * 번개 모임 목록 조회 + * @return:모임 목록 DTO 배열 + */ + async getMeetings(userId) { + const meetings = await Meeting.findAll({ + attributes: ['id', 'title', 'description', 'start_time', 'end_time', 'location', 'deadline', 'type'], + include: [ + { + model: User, + as: 'creator', + attributes: ['name'], + }, + { + model: MeetingParticipant, + as: 'participants', + attributes: ['user_id'], + }, + ], + }); + + return meetings.map((meeting) => { + const creatorName = meeting.creator ? meeting.creator.name : 'Unknown'; + const isParticipant = meeting.participants.some(participant => participant.user_id === parseInt(userId, 10)); + + return new MeetingResponseDTO( + meeting, + isParticipant, + false, // isScheduleConflict: 필요 시 추가 로직 구현 + creatorName + ); + }); } - meeting.type = 'CLOSE'; - await meeting.save(); - return meeting; - } + /** + * 번개 모임 마감 + * @returns 마감된 모임 객체 + */ + async closeMeeting(meetingId) { + const meeting = await Meeting.findByPk(meetingId); + if (!meeting) { + throw new Error('모임을 찾을 수 없습니다.'); + } - async joinMeeting(meetingId, userId) { - const meeting = await Meeting.findByPk(meetingId); - if (!meeting) { - throw new Error('모임을 찾을 수 없습니다.'); - } + if (meeting.type === 'CLOSE') { + throw new Error('이미 마감된 모임입니다.'); + } - if(meeting.type === 'CLOSE') { - throw new Error('이미 마감된 모임입니다.'); + meeting.type = 'CLOSE'; + await meeting.save(); + return meeting; } - if (new Date() > new Date(meeting.deadline)) { - throw new Error('참가 신청이 마감되었습니다.'); - } + + //번개모임 참가 + async joinMeeting(meetingId, userId) { + const meeting = await Meeting.findByPk(meetingId); + if (!meeting) { + throw new Error('모임을 찾을 수 없습니다.'); + } - const existingParticipant = await MeetingParticipant.findOne({ - where: { meeting_id: meetingId, user_id: userId } - }); - - if (existingParticipant) { - throw new Error('이미 참가한 사용자입니다.'); - } + if (meeting.type === 'CLOSE') { + throw new Error('이미 마감된 모임입니다.'); + } - await MeetingParticipant.create({ meeting_id: meetingId, user_id: userId }); + if (new Date() > new Date(meeting.deadline)) { + throw new Error('참가 신청이 마감되었습니다.'); + } - const user = await User.findOne({ where: { id: userId } }); - const chatRoom = await ChatRoom.findOne({ meeting_id: meetingId }); + const existingParticipant = await MeetingParticipant.findOne({ + where: { meeting_id: meetingId, user_id: userId } + }); - 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(); - } - } - - async getMeetingDetail(meetingId) { - const meeting = await Meeting.findByPk(meetingId, { - include: [ - { - model: User, - as: 'creator', - attributes: ['name'] - }, - { - model: MeetingParticipant, - as: 'participants', - include: [ - { - model: User, - as: 'participantUser', - attributes: ['name', 'email'] - } - ] + if (existingParticipant) { + throw new Error('이미 참가한 사용자입니다.'); } - ] - }); - if (!meeting) { - throw new Error('모임을 찾을 수 없습니다.'); + // 트랜잭션을 사용하여 참가자 추가 및 스케줄 업데이트를 원자적으로 처리 + await Meeting.sequelize.transaction(async (transaction) => { + // 참가자 추가 + await MeetingParticipant.create({ meeting_id: meetingId, user_id: userId }, { transaction }); + + // 스케줄 충돌 확인 + const hasConflict = await ScheduleService.checkScheduleOverlap( + userId, + new Date(meeting.start_time), + new Date(meeting.end_time) + ); + if (hasConflict) { + throw new Error('스케줄이 겹칩니다. 다른 모임에 참가하세요.'); + } + + // 스케줄 추가 + await ScheduleService.createSchedule({ + userId: userId, + title: `번개 모임: ${meeting.title}`, + start_time: new Date(meeting.start_time), + end_time: new Date(meeting.end_time), + is_fixed: true, + }); + + // 채팅방 참가 + const user = await User.findOne({ where: { id: userId } }); + const chatRoom = await ChatRoom.findOne({ where: { meeting_id: meetingId } }); + + 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(); + } + }); } - return new MeetingDetailResponseDTO(meeting); - } + /** + * 번개 모임 상세 조회 + * @return 모임 상세 DTO + */ + async getMeetingDetail(meetingId) { + const meeting = await Meeting.findByPk(meetingId, { + include: [ + { + model: User, + as: 'creator', + attributes: ['name'] + }, + { + model: MeetingParticipant, + as: 'participants', + include: [ + { + model: User, + as: 'participantUser', + attributes: ['name', 'email'] + } + ] + } + ] + }); + + if (!meeting) { + throw new Error('모임을 찾을 수 없습니다.'); + } + + return new MeetingDetailResponseDTO(meeting); + } } module.exports = new MeetingService(); -- GitLab