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