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미팅방 서비스 관련 검증 로직 보강 및 트랜잭션 적용
This commit is part of merge request !19. Comments created here will be created in the context of that merge request.
// 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();
// 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) {
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;
// 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;
// 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();
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