Skip to content
Snippets Groups Projects
Commit b3bea64d authored by tpgus2603's avatar tpgus2603
Browse files

refactor,feature: 트랜잭션 적용 로직 및 검증로직 보강(#12)

parent a8df7a09
No related branches found
No related tags found
2 merge requests!31Develop,!19미팅방 서비스 관련 검증 로직 보강 및 트랜잭션 적용
// controllers/meetingController.js
const MeetingService = require('../services/meetingService'); const MeetingService = require('../services/meetingService');
const CreateMeetingRequestDTO = require('../dtos/CreateMeetingRequestDTO');
class MeetingController { class MeetingController {
async createMeeting(req, res) { /**
try { * 번개 모임 생성
const result = await MeetingService.createMeeting(req.body); * POST /api/meetings
res.status(201).json(result); */
} catch (err) { async createMeeting(req, res) {
console.error('번개 모임 생성 오류:', err); try {
res.status(500).json({ error: err.message || '번개 모임 생성 실패' }); const userId = req.userId; // 인증 미들웨어를 통해 설정된 사용자 ID
} const meetingData = { ...req.body, created_by: userId };
}
async getMeetings(req, res) { // CreateMeetingRequestDTO를 사용하여 요청 데이터 검증
const { userId } = req.query; const createMeetingDTO = new CreateMeetingRequestDTO(meetingData);
createMeetingDTO.validate();
if (!userId) { const result = await MeetingService.createMeeting(meetingData);
return res.status(400).json({ error: '사용자 ID가 필요합니다.' }); 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); * GET /api/meetings
} catch (err) { */
console.error('모임 목록 조회 오류:', err); async getMeetings(req, res) {
res.status(500).json({ error: err.message || '모임 목록 조회 실패' }); 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 { 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 });
} catch (err) { } catch (err) {
console.error('모임 마감 오류:', err); console.error('모임 마감 오류:', err);
res.status(500).json({ error: err.message || '모임 마감 실패' }); 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, userId);
await MeetingService.joinMeeting(meetingId, user_id); res.status(200).json({ message: '모임 및 채팅방 참가 완료' });
res.status(200).json({ message: '모임 및 채팅방 참가 완료' }); } catch (err) {
} catch (err) { console.error('모임 참가 오류:', err);
console.error('모임 참가 오류:', err); res.status(500).json({ error: err.message || '모임 참가 실패' });
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 { try {
const meetingDetail = await MeetingService.getMeetingDetail(meetingId); const meetingDetail = await MeetingService.getMeetingDetail(meetingId);
res.status(200).json(meetingDetail); res.status(200).json(meetingDetail);
} catch (err) { } catch (err) {
console.error('모임 상세 조회 오류:', err); console.error('모임 상세 조회 오류:', err);
res.status(500).json({ error: err.message || '모임 상세 조회 실패' }); res.status(500).json({ error: err.message || '모임 상세 조회 실패' });
}
} }
}
} }
module.exports = new MeetingController(); module.exports = new MeetingController();
\ No newline at end of file
// 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;
class MeetingDetailResponse { // dtos/MeetingDetailResponseDTO.js
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.startTime = meeting.start_time;
this.endTime = meeting.end_time; this.endTime = meeting.end_time;
this.location = meeting.location; this.location = meeting.location;
this.deadline = meeting.deadline; this.deadline = meeting.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 => ({
userId: participant.user_id, userId: participant.user_id,
name: participant.participantUser ? participant.participantUser.name : 'Unknown', name: participant.participantUser ? participant.participantUser.name : 'Unknown',
email: participant.participantUser ? participant.participantUser.email : 'Unknown' email: participant.participantUser ? participant.participantUser.email : 'Unknown'
})); }));
} }
} }
module.exports = MeetingDetailResponse; module.exports = MeetingDetailResponseDTO;
\ No newline at end of file
// dtos/MeetingResponse.js // dtos/MeetingResponseDTO.js
class MeetingResponse { 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.startTime = meeting.start_time;
this.endTime = meeting.end_time; this.endTime = meeting.end_time;
this.location = meeting.location; this.location = meeting.location;
this.deadline = meeting.deadline; this.deadline = meeting.deadline;
this.type = meeting.type; this.type = meeting.type;
this.creatorName = creatorName; this.creatorName = creatorName;
this.isParticipant = isParticipant; this.isParticipant = isParticipant;
this.isScheduleConflict = isScheduleConflict; this.isScheduleConflict = isScheduleConflict;
} }
} }
module.exports = MeetingResponse; module.exports = MeetingResponseDTO;
\ No newline at end of file
// services/meetingService.js
const { v4: uuidv4 } = require('uuid'); 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 ChatRoom = require('../models/chatRooms');
const chatController = require('../controllers/chatController'); const chatController = require('../controllers/chatController');
const MeetingResponse = require('../dtos/MeetingResponse'); const MeetingResponseDTO = require('../dtos/MeetingResponseDTO');
const MeetingDetailResponseDTO = require('../dtos/MeetingDetailResponseDTO'); const MeetingDetailResponseDTO = require('../dtos/MeetingDetailResponseDTO');
const CreateMeetingRequestDTO = require('../dtos/CreateMeetingRequestDTO');
const ScheduleService = require('./scheduleService'); // ScheduleService 임포트
class MeetingService { 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) { const hasConflict = await ScheduleService.checkScheduleOverlap(
throw new Error('사용자를 찾을 수 없습니다.'); created_by,
} new Date(start_time),
new Date(end_time)
);
if (hasConflict) {
throw new Error('스케줄이 겹칩니다. 다른 시간을 선택해주세요.');
}
const chatRoomData = { // 트랜잭션을 사용하여 모임 생성과 스케줄 추가를 원자적으로 처리
participants: [user.name], const result = await Meeting.sequelize.transaction(async (transaction) => {
}; // 채팅방 생성
const chatRoomResponse = await chatController.createChatRoomInternal(chatRoomData); const chatRoomData = {
participants: [user.name],
};
const chatRoomResponse = await chatController.createChatRoomInternal(chatRoomData);
if (!chatRoomResponse.success) { if (!chatRoomResponse.success) {
throw new Error('채팅방 생성 실패'); throw new Error('채팅방 생성 실패');
} }
const chatRoomId = chatRoomResponse.chatRoomId; const chatRoomId = chatRoomResponse.chatRoomId;
const newMeeting = await Meeting.create({ // 모임 생성
title, const newMeeting = await Meeting.create({
description, title,
start_time, description,
end_time, start_time,
location, end_time,
deadline, location,
type, deadline,
created_by, type,
chatRoomId, created_by,
}); chatRoomId,
}, { transaction });
await MeetingParticipant.create({
meeting_id: newMeeting.id, // 모임 참가자 추가 (생성자 자신)
user_id: created_by, await MeetingParticipant.create({
}); meeting_id: newMeeting.id,
user_id: created_by,
return { meeting_id: newMeeting.id, chatRoomId: chatRoomResponse.chatRoomId }; }, { transaction });
}
// 스케줄 추가
async getMeetings(userId) { await ScheduleService.createSchedule({
const meetings = await Meeting.findAll({ userId: created_by,
attributes: ['id', 'title', 'description', 'start_time', 'end_time', 'location', 'deadline', 'type'], title: `번개 모임: ${title}`,
include: [ start_time: new Date(start_time),
{ end_time: new Date(end_time),
model: User, is_fixed: true,
as: 'creator', });
attributes: ['name'],
}, return { meeting_id: newMeeting.id, chatRoomId };
{ });
model: MeetingParticipant,
as: 'participants', return result;
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('모임을 찾을 수 없습니다.');
} }
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) { if (meeting.type === 'CLOSE') {
const meeting = await Meeting.findByPk(meetingId); throw new Error('이미 마감된 모임입니다.');
if (!meeting) { }
throw new Error('모임을 찾을 수 없습니다.');
}
if(meeting.type === 'CLOSE') { meeting.type = 'CLOSE';
throw new Error('이미 마감된 모임입니다.'); 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({ if (meeting.type === 'CLOSE') {
where: { meeting_id: meetingId, user_id: userId } throw new Error('이미 마감된 모임입니다.');
}); }
if (existingParticipant) {
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 existingParticipant = await MeetingParticipant.findOne({
const chatRoom = await ChatRoom.findOne({ meeting_id: meetingId }); where: { meeting_id: meetingId, user_id: userId }
});
if (chatRoom && !chatRoom.participants.includes(user.name)) { if (existingParticipant) {
chatRoom.participants.push(user.name); throw new Error('이미 참가한 사용자입니다.');
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 (!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(); module.exports = new MeetingService();
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment