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

[#] 모델연관관계 중앙화

parents 3019d3cb 7e0fe4e0
No related branches found
No related tags found
2 merge requests!31Develop,!20모델연관관계 중앙화
......@@ -20,7 +20,7 @@ class CreateMeetingRequestDTO {
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(),
deadline: Joi.date().iso().less(Joi.ref('start_time')).optional(),
type: Joi.string().valid('OPEN', 'CLOSE').required(),
created_by: Joi.number().integer().positive().required()
});
......
......@@ -25,11 +25,11 @@ const Friend = sequelize.define('Friend', {
]
});
// 관계 설정
Friend.belongsTo(User, { foreignKey: 'requester_id', as: 'requester' }); // 친구 요청을 보낸 사용자
Friend.belongsTo(User, { foreignKey: 'receiver_id', as: 'receiver' }); // 친구 요청을 받은 사용자
// // 관계 설정
// 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' }); // 친구 요청을 받은 목록
// User.hasMany(Friend, { foreignKey: 'requester_id', as: 'sentRequests' }); // 친구 요청을 보낸 목록
// User.hasMany(Friend, { foreignKey: 'receiver_id', as: 'receivedRequests' }); // 친구 요청을 받은 목록
module.exports = Friend;
// models/index.js
const sequelize = require('../config/sequelize');
const User = require('./User');
const Friend = require('./Friend');
const Schedule = require('./Schedule');
const Meeting = require('./Meeting');
const MeetingParticipant = require('./MeetingParticipant'); //폴더명수정
const Friend = require('./Friend');
const MeetingParticipant = require('./MeetingParticipant');
const ChatRoom = require('./ChatRooms');
// 관계 설정
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' }); // 친구 요청을 받은 목록
// 연관 관계 설정
Meeting.belongsTo(User, { foreignKey: 'created_by', as: 'creator' });
User.hasMany(Meeting, { foreignKey: 'created_by', as: 'meetings' });
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' });
Schedule.belongsTo(User, { foreignKey: 'user_id', as: 'user' });
User.hasMany(Schedule, { foreignKey: 'user_id', as: 'schedules' });
module.exports = {
sequelize,
User,
Schedule,
Meeting,
MeetingParticipant,
Friend,
sequelize,
User,
Friend,
Schedule,
Meeting,
MeetingParticipant,
ChatRoom,
};
......@@ -44,9 +44,9 @@ const Meeting = sequelize.define('Meeting', {
});
// 연관 관계 설정
Meeting.belongsTo(User, { foreignKey: 'created_by', as: 'creator' });
User.hasMany(Meeting, { foreignKey: 'created_by', as: 'meetings' });
// // 연관 관계 설정
// Meeting.belongsTo(User, { foreignKey: 'created_by', as: 'creator' });
// User.hasMany(Meeting, { foreignKey: 'created_by', as: 'meetings' });
module.exports = Meeting;
......@@ -20,11 +20,11 @@ const MeetingParticipant = sequelize.define('MeetingParticipant', {
timestamps: false,
});
MeetingParticipant.belongsTo(Meeting, { foreignKey: 'meeting_id', as: 'meeting' });
Meeting.hasMany(MeetingParticipant, { foreignKey: 'meeting_id', as: 'participants' });
// 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' });
// MeetingParticipant.belongsTo(User, { foreignKey: 'user_id', as: 'user' });
// User.hasMany(MeetingParticipant, { foreignKey: 'user_id', as: 'meetingParticipations' });
module.exports = MeetingParticipant;
......@@ -30,7 +30,7 @@ const Schedule = sequelize.define('Schedule', {
timestamps: true, // created_at과 updated_at 자동 관리
});
Schedule.belongsTo(User, { foreignKey: 'user_id', as: 'user' });
User.hasMany(Schedule, { foreignKey: 'user_id', as: 'schedules' });
// Schedule.belongsTo(User, { foreignKey: 'user_id', as: 'user' });
// User.hasMany(Schedule, { foreignKey: 'user_id', as: 'schedules' });
module.exports = Schedule;
\ No newline at end of file
......@@ -5,11 +5,11 @@ const sequelize = require('../config/sequelize');
const User = sequelize.define('User', {
name: {
type: DataTypes.STRING, // VARCHAR
type: DataTypes.STRING,
allowNull: false,
},
email: {
type: DataTypes.STRING, // VARCHAR
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
......
const ChatRoom = require('../models/chatRooms');
const ChatRoom = require('../models/ChatRooms');
const { v4: uuidv4 } = require('uuid');
class ChatService {
......
// services/friendService.js
const { Op } = require('sequelize');
const Friend = require('../models/Friend');
const User = require('../models/User');
const { Friend,User} = require('../models');
const sequelize = require('../config/sequelize');
// DTO 임포트
......
// test/friendService.test.js
const sequelize = require('../config/sequelize'); // Sequelize 인스턴스 임포트
const User = require('../models/User');
const Friend = require('../models/Friend');
// const User = require('../models/User');
// const Friend = require('../models/Friend');
const { Friend,User} = require('../models');
const friendService = require('./friendService'); // FriendService 임포트
// Sequelize의 Op를 가져오기 위해 추가
......
// test/friend.test.js
const sequelize = require('../config/sequelize'); // 환경 변수에 따라 다른 인스턴스 사용
const User = require('../models/User');
const Friend = require('../models/Friend');
const { Friend,User} = require('../models');
beforeAll(async () => {
// 데이터베이스 동기화
......
......@@ -2,8 +2,8 @@
const { v4: uuidv4 } = require('uuid');
const { Op } = require('sequelize');
const { Meeting, MeetingParticipant, User, Schedule } = require('../models');
const ChatRoom = require('../models/chatRooms');
const { Meeting, MeetingParticipant, User, Schedule } = require('../models'); // models/index.js를 통해 임포트
const ChatRoom = require('../models/ChatRooms');
const chatController = require('../controllers/chatController');
const MeetingResponseDTO = require('../dtos/MeetingResponseDTO');
const MeetingDetailResponseDTO = require('../dtos/MeetingDetailResponseDTO');
......@@ -13,7 +13,8 @@ const ScheduleService = require('./scheduleService'); // ScheduleService 임포
class MeetingService {
/**
* 번개 모임 생성
* @returns 생성된 모임 ID와 채팅방 ID
* @param {object} meetingData - 모임 생성 데이터
* @returns {Promise<object>} - 생성된 모임 ID와 채팅방 ID
*/
async createMeeting(meetingData) {
// DTO를 사용하여 요청 데이터 검증
......@@ -39,7 +40,7 @@ class MeetingService {
}
// 트랜잭션을 사용하여 모임 생성과 스케줄 추가를 원자적으로 처리
const result = await Meeting.sequelize.transaction(async (transaction) => {
const result = await ScheduleService.withTransaction(async (transaction) => {
// 채팅방 생성
const chatRoomData = {
participants: [user.name],
......@@ -71,14 +72,14 @@ class MeetingService {
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,
});
}, transaction);
return { meeting_id: newMeeting.id, chatRoomId };
});
......@@ -88,7 +89,8 @@ class MeetingService {
/**
* 번개 모임 목록 조회
* @return:모임 목록 DTO 배열
* @param {number} userId - 사용자 ID
* @returns {Promise<Array<MeetingResponseDTO>>} - 모임 목록 DTO 배열
*/
async getMeetings(userId) {
const meetings = await Meeting.findAll({
......@@ -122,7 +124,8 @@ class MeetingService {
/**
* 번개 모임 마감
* @returns 마감된 모임 객체
* @param {number} meetingId - 모임 ID
* @returns {Promise<Meeting>} - 마감된 모임 객체
*/
async closeMeeting(meetingId) {
const meeting = await Meeting.findByPk(meetingId);
......@@ -139,8 +142,12 @@ class MeetingService {
return meeting;
}
//번개모임 참가
/**
* 번개 모임 참가
* @param {number} meetingId - 모임 ID
* @param {number} userId - 사용자 ID
* @returns {Promise<void>}
*/
async joinMeeting(meetingId, userId) {
const meeting = await Meeting.findByPk(meetingId);
if (!meeting) {
......@@ -164,7 +171,7 @@ class MeetingService {
}
// 트랜잭션을 사용하여 참가자 추가 및 스케줄 업데이트를 원자적으로 처리
await Meeting.sequelize.transaction(async (transaction) => {
await ScheduleService.withTransaction(async (transaction) => {
// 참가자 추가
await MeetingParticipant.create({ meeting_id: meetingId, user_id: userId }, { transaction });
......@@ -178,32 +185,33 @@ class MeetingService {
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,
});
}, transaction);
// 채팅방 참가
const user = await User.findOne({ where: { id: userId } });
const chatRoom = await ChatRoom.findOne({ where: { meeting_id: meetingId } });
const user = await User.findOne({ where: { id: userId }, transaction });
const chatRoom = await ChatRoom.findOne({ where: { meeting_id: meetingId }, transaction });
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();
await chatRoom.save({ transaction });
}
});
}
/**
* 번개 모임 상세 조회
* @return 모임 상세 DTO
* @param {number} meetingId - 모임 ID
* @returns {Promise<MeetingDetailResponseDTO>} - 모임 상세 DTO
*/
async getMeetingDetail(meetingId) {
const meeting = await Meeting.findByPk(meetingId, {
......
// test/meetingService.test.js
const { sequelize, User, Friend, Schedule, Meeting, MeetingParticipant, ChatRoom } = require('../models'); // models/index.js를 통해 임포트
const MeetingService = require('../services/meetingService');
const ScheduleService = require('../services/scheduleService'); // ScheduleService 임포트
const chatController = require('../controllers/chatController');
// Jest를 사용하여 chatController 모킹
jest.mock('../controllers/chatController', () => ({
createChatRoomInternal: jest.fn()
}));
describe('MeetingService', () => {
beforeAll(async () => {
await sequelize.sync({ force: true });
// 더미 사용자 생성
await User.bulkCreate([
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
]);
// 더미 친구 관계 생성
await Friend.create({
id: 1,
requester_id: 1,
receiver_id: 2,
status: 'ACCEPTED',
});
// 더미 스케줄 생성
await Schedule.bulkCreate([
{
id: 1,
user_id: 1,
title: "Alice's Fixed Schedule",
start_time: new Date('2024-05-01T09:00:00Z'),
end_time: new Date('2024-05-01T10:00:00Z'),
is_fixed: true,
expiry_date: null,
},
{
id: 2,
user_id: 1,
title: "Alice's Flexible Schedule",
start_time: new Date('2024-05-02T11:00:00Z'),
end_time: new Date('2024-05-02T12:00:00Z'),
is_fixed: false,
expiry_date: new Date('2024-05-08T00:00:00Z'), // 다음 월요일
},
]);
// 기본적인 채팅방 생성 모킹 설정 (성공)
chatController.createChatRoomInternal.mockResolvedValue({
success: true,
chatRoomId: 'chatroom-1234'
});
});
afterAll(async () => {
// 데이터베이스 연결 종료
await sequelize.close();
});
beforeEach(() => {
// 각 테스트 전에 mock 호출 이력 초기화
jest.clearAllMocks();
});
describe('createMeeting', () => {
test('번개 모임을 성공적으로 생성해야 한다', async () => {
const meetingData = {
title: 'Tech Talk',
description: 'A discussion on the latest tech trends.',
start_time: '2024-05-10T10:00:00Z',
end_time: '2024-05-10T12:00:00Z',
location: 'Online',
deadline: '2024-05-09T23:59:59Z',
type: 'OPEN',
created_by: 1,
};
const result = await MeetingService.createMeeting(meetingData);
expect(result).toHaveProperty('meeting_id');
expect(result).toHaveProperty('chatRoomId');
expect(result.chatRoomId).toBe('chatroom-1234');
// 모임이 DB에 생성되었는지 확인
const meeting = await Meeting.findByPk(result.meeting_id);
expect(meeting).toBeDefined();
expect(meeting.title).toBe('Tech Talk');
// 참가자가 추가되었는지 확인
const participant = await MeetingParticipant.findOne({
where: { meeting_id: result.meeting_id, user_id: 1 }
});
expect(participant).toBeDefined();
// 스케줄이 추가되었는지 확인
const schedule = await Schedule.findOne({
where: { user_id: 1, title: '번개 모임: Tech Talk' }
});
expect(schedule).toBeDefined();
expect(schedule.start_time.toISOString()).toBe('2024-05-10T10:00:00.000Z');
expect(schedule.end_time.toISOString()).toBe('2024-05-10T12:00:00.000Z');
expect(schedule.is_fixed).toBe(true);
expect(schedule.expiry_date).toBeNull();
// chatController.createChatRoomInternal이 호출되었는지 확인
expect(chatController.createChatRoomInternal).toHaveBeenCalledTimes(1);
expect(chatController.createChatRoomInternal).toHaveBeenCalledWith({
participants: ['Alice']
});
});
test('사용자의 스케줄이 겹치는 경우 모임 생성을 실패해야 한다', async () => {
// Alice의 기존 스케줄 생성 (이미 beforeAll에서 생성됨)
const overlappingMeetingData = {
title: 'Overlap Meeting',
description: 'This meeting overlaps with Alice\'s fixed schedule.',
start_time: '2024-05-01T09:30:00Z', // Alice's Fixed Schedule과 겹침
end_time: '2024-05-01T11:00:00Z',
location: 'Office',
deadline: '2024-04-30T23:59:59Z',
type: 'OPEN',
created_by: 1,
};
await expect(MeetingService.createMeeting(overlappingMeetingData))
.rejects
.toThrow('스케줄이 겹칩니다. 다른 시간을 선택해주세요.');
});
test('모임 생성 시 스케줄의 유동 스케줄이 만료되면 스케줄 충돌이 발생하지 않아야 한다', async () => {
const meetingData = {
title: 'Morning Meeting',
description: 'Meeting after flexible schedule expiry.',
start_time: '2024-05-09T09:00:00Z', // Flexible Schedule의 expiry_date가 지난 시점
end_time: '2024-05-09T10:00:00Z',
location: 'Conference Room',
deadline: '2024-05-08T23:59:59Z',
type: 'OPEN',
created_by: 1,
};
const result = await MeetingService.createMeeting(meetingData);
expect(result).toHaveProperty('meeting_id');
expect(result).toHaveProperty('chatRoomId');
expect(result.chatRoomId).toBe('chatroom-1234');
// 스케줄이 추가되었는지 확인
const schedule = await Schedule.findOne({
where: { user_id: 1, title: '번개 모임: Morning Meeting' }
});
expect(schedule).toBeDefined();
expect(schedule.start_time.toISOString()).toBe('2024-05-09T09:00:00.000Z');
expect(schedule.end_time.toISOString()).toBe('2024-05-09T10:00:00.000Z');
expect(schedule.is_fixed).toBe(true);
expect(schedule.expiry_date).toBeNull();
});
test('모임 생성 시 채팅방 생성 실패하면 에러를 던져야 한다', async () => {
// chatController.createChatRoomInternal을 실패하도록 모킹
chatController.createChatRoomInternal.mockResolvedValueOnce({
success: false
});
const meetingData = {
title: 'Failed ChatRoom Meeting',
description: 'This meeting will fail to create chat room.',
start_time: '2024-05-11T10:00:00Z',
end_time: '2024-05-11T12:00:00Z',
location: 'Online',
deadline: '2024-05-10T23:59:59Z',
type: 'OPEN',
created_by: 1,
};
await expect(MeetingService.createMeeting(meetingData))
.rejects
.toThrow('채팅방 생성 실패');
});
test('모임 생성 시 유효하지 않은 데이터는 검증 에러를 던져야 한다', async () => {
const invalidMeetingData = {
title: '', // 빈 제목
start_time: 'invalid-date',
end_time: '2024-05-10T09:00:00Z', // start_time보다 이전
type: 'INVALID_TYPE',
created_by: -1, // 음수 ID
};
await expect(MeetingService.createMeeting(invalidMeetingData))
.rejects
.toThrow('Validation error');
});
});
describe('joinMeeting', () => {
test('번개 모임에 성공적으로 참가해야 한다', async () => {
// Alice가 모임 생성
const meetingData = {
title: 'Networking Event',
description: 'An event to network with professionals.',
start_time: '2024-06-15T10:00:00Z',
end_time: '2024-06-15T12:00:00Z',
location: 'Conference Hall',
deadline: '2024-06-14T23:59:59Z',
type: 'OPEN',
created_by: 1,
};
const { meeting_id } = await MeetingService.createMeeting(meetingData);
// Bob가 모임에 참가
await MeetingService.joinMeeting(meeting_id, 2);
// 참가자가 추가되었는지 확인
const participant = await MeetingParticipant.findOne({
where: { meeting_id: meeting_id, user_id: 2 }
});
expect(participant).toBeDefined();
// 스케줄이 추가되었는지 확인
const schedule = await Schedule.findOne({
where: { user_id: 2, title: '번개 모임: Networking Event' }
});
expect(schedule).toBeDefined();
expect(schedule.start_time.toISOString()).toBe('2024-06-15T10:00:00.000Z');
expect(schedule.end_time.toISOString()).toBe('2024-06-15T12:00:00.000Z');
expect(schedule.is_fixed).toBe(true);
expect(schedule.expiry_date).toBeNull();
// chatController.createChatRoomInternal이 호출되지 않았는지 확인 (이미 모임 생성 시 호출됨)
expect(chatController.createChatRoomInternal).toHaveBeenCalledTimes(1);
});
test('모임 참가 시 스케줄이 겹치는 경우 참가를 실패해야 한다', async () => {
// Alice가 모임 생성
const meetingData1 = {
title: 'Morning Yoga',
description: 'Start your day with yoga.',
start_time: '2024-07-01T06:00:00Z',
end_time: '2024-07-01T07:00:00Z',
location: 'Gym',
deadline: '2024-06-30T23:59:59Z',
type: 'OPEN',
created_by: 1,
};
const { meeting_id: meetingId1 } = await MeetingService.createMeeting(meetingData1);
// Bob의 기존 스케줄 생성
await ScheduleService.createSchedule({
userId: 2,
title: 'Work',
start_time: new Date('2024-07-01T06:30:00Z'),
end_time: new Date('2024-07-01T08:30:00Z'),
is_fixed: true,
}, null); // 트랜잭션 없이 생성
// Bob가 모임에 참가 시도
await expect(MeetingService.joinMeeting(meetingId1, 2))
.rejects
.toThrow('스케줄이 겹칩니다. 다른 모임에 참가하세요.');
});
test('모임 참가 시 이미 참가한 사용자는 다시 참가할 수 없어야 한다', async () => {
// Alice가 모임 생성
const meetingData = {
title: 'Evening Run',
description: 'Join us for an evening run.',
start_time: '2024-08-20T18:00:00Z',
end_time: '2024-08-20T19:00:00Z',
location: 'Park',
deadline: '2024-08-19T23:59:59Z',
type: 'OPEN',
created_by: 1,
};
const { meeting_id } = await MeetingService.createMeeting(meetingData);
// Bob가 모임에 참가
await MeetingService.joinMeeting(meeting_id, 2);
// Bob가 다시 모임에 참가하려 시도
await expect(MeetingService.joinMeeting(meeting_id, 2))
.rejects
.toThrow('이미 참가한 사용자입니다.');
});
test('모임 마감된 경우 참가를 실패해야 한다', async () => {
// Alice가 모임 생성 (이미 마감됨)
const meetingData = {
title: 'Afternoon Workshop',
description: 'A workshop on web development.',
start_time: '2024-09-10T14:00:00Z',
end_time: '2024-09-10T16:00:00Z',
location: 'Office',
deadline: '2024-09-09T23:59:59Z',
type: 'CLOSE',
created_by: 1,
};
const { meeting_id } = await MeetingService.createMeeting(meetingData);
// Bob가 모임에 참가 시도
await expect(MeetingService.joinMeeting(meeting_id, 2))
.rejects
.toThrow('이미 마감된 모임입니다.');
});
});
});
// services/scheduleService.js
const sequelize = require('../config/sequelize');
const { Op } = require('sequelize');
const Schedule = require('../models/Schedule');
const ScheduleResponseDTO = require('../dtos/ScheduleResponseDTO');
......@@ -9,16 +9,16 @@ class scheduleService {
* 트랜잭션 래퍼 함수
*/
async withTransaction(callback) {
const transaction = await Schedule.sequelize.transaction();
try {
const result = await callback(transaction);
await transaction.commit();
return result;
} catch (error) {
await transaction.rollback();
throw error;
const transaction = await sequelize.transaction(); // 직접 sequelize 사용
try {
const result = await callback(transaction);
await transaction.commit();
return result;
} catch (error) {
await transaction.rollback();
throw error;
}
}
}
/**
* 공통 where 절 생성
......
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