diff --git a/services/friendService.js b/services/friendService.js index 0111e1fc4681c7b89acf1802ab2a01e7205bbfdc..f0dd45336a6892030759fcd77b2763b5c1a26979 100644 --- a/services/friendService.js +++ b/services/friendService.js @@ -3,7 +3,11 @@ const { Op } = require('sequelize'); const Friend = require('../models/Friend'); const User = require('../models/User'); -const sequelize = require('../config/sequelize'); // 트랜잭션을 위해 추가 +const sequelize = require('../config/sequelize'); + +// DTO 임포트 +const FriendRequestDTO = require('../dtos/FriendRequestDTO'); +const FriendListDTO = require('../dtos/FriendListDTO'); class FriendService { /** @@ -24,7 +28,7 @@ class FriendService { * 친구 요청 보내기 * @param {number} userId - 친구 요청을 보내는 사용자 ID * @param {number} friendId - 친구 요청을 받는 사용자 ID - * @returns {Promise<Friend>} - 생성된 친구 요청 객체 + * @returns {Promise<FriendRequestDTO>} - 생성된 친구 요청 DTO * @throws {Error} - 유효하지 않은 요청일 경우 */ async sendFriendRequest(userId, friendId) { @@ -35,12 +39,40 @@ class FriendService { throw new Error('Cannot send friend request to yourself'); } + // 기존 친구 관계 확인 (이미 친구인 경우) + const existingFriend = await Friend.findOne({ + where: { + [Op.or]: [ + { requester_id: userId, receiver_id: friendId }, + { requester_id: friendId, receiver_id: userId }, + ], + status: 'ACCEPTED', + }, + }); + + if (existingFriend) { + throw new Error('Friend request already exists'); + } + try { - return await Friend.create({ + const friendRequest = await Friend.create({ requester_id: userId, receiver_id: friendId, status: 'PENDING' }); + + // DTO로 변환하여 반환 + const friendRequestWithDetails = await Friend.findByPk(friendRequest.id, { + include: [ + { model: User, as: 'requester', attributes: ['id', 'name', 'email'] }, + { model: User, as: 'receiver', attributes: ['id', 'name', 'email'] } + ] + }); + + // 디버깅을 위해 로그 추가 + //console.log('FriendRequestWithDetails:', friendRequestWithDetails.toJSON()); + + return new FriendRequestDTO(friendRequestWithDetails.toJSON()); } catch (error) { if (error.name === 'SequelizeUniqueConstraintError') { throw new Error('Friend request already exists'); @@ -52,46 +84,48 @@ class FriendService { /** * 받은 친구 요청 목록 조회 * @param {number} userId - 요청을 받은 사용자 ID - * @returns {Promise<Array>} - 받은 친구 요청 목록 + * @returns {Promise<Array<FriendRequestDTO>>} - 받은 친구 요청 목록 DTO 배열 */ async getReceivedRequests(userId) { - return Friend.findAll({ + const receivedRequests = await Friend.findAll({ where: { receiver_id: userId, status: 'PENDING' }, - include: [{ - model: User, - as: 'requester', - attributes: ['id', 'name', 'email'] - }] + include: [ + { model: User, as: 'requester', attributes: ['id', 'name', 'email'] }, + { model: User, as: 'receiver', attributes: ['id', 'name', 'email'] } // 추가 + ] }); + + return receivedRequests.map(req => new FriendRequestDTO(req)); } /** * 보낸 친구 요청 목록 조회 * @param {number} userId - 요청을 보낸 사용자 ID - * @returns {Promise<Array>} - 보낸 친구 요청 목록 + * @returns {Promise<Array<FriendRequestDTO>>} - 보낸 친구 요청 목록 DTO 배열 */ async getSentRequests(userId) { - return Friend.findAll({ + const sentRequests = await Friend.findAll({ where: { requester_id: userId, status: 'PENDING' }, - include: [{ - model: User, - as: 'receiver', - attributes: ['id', 'name', 'email'] - }] + include: [ + { model: User, as: 'receiver', attributes: ['id', 'name', 'email'] }, + { model: User, as: 'requester', attributes: ['id', 'name', 'email'] } // 추가 + ] }); + + return sentRequests.map(req => new FriendRequestDTO(req)); } /** * 친구 요청 수락 * @param {number} userId - 요청을 수락하는 사용자 ID * @param {number} friendId - 친구 요청을 보낸 사용자 ID - * @returns {Promise<Friend>} - 업데이트된 친구 요청 객체 + * @returns {Promise<FriendRequestDTO>} - 업데이트된 친구 요청 DTO * @throws {Error} - 친구 요청이 존재하지 않을 경우 */ async acceptFriendRequest(userId, friendId) { @@ -113,7 +147,16 @@ class FriendService { await request.update({ status: 'ACCEPTED' }, { transaction }); await transaction.commit(); - return request; + + // DTO로 변환하여 반환 + const updatedRequest = await Friend.findByPk(request.id, { + include: [ + { model: User, as: 'requester', attributes: ['id', 'name', 'email'] }, + { model: User, as: 'receiver', attributes: ['id', 'name', 'email'] } + ] + }); + + return new FriendRequestDTO(updatedRequest); } catch (error) { await transaction.rollback(); throw error; @@ -146,9 +189,11 @@ class FriendService { /** * 친구 목록 조회 * @param {number} userId - 친구 목록을 조회할 사용자 ID - * @returns {Promise<Array>} - 친구 목록 + * @param {number} limit - 한 페이지에 표시할 친구 수 + * @param {number} offset - 페이징 오프셋 + * @returns {Promise<Array<FriendListDTO>>} - 친구 목록 DTO 배열 */ - async getFriendList(userId) { + async getFriendList(userId, limit = 20, offset = 0) { const friends = await Friend.findAll({ where: { [Op.or]: [ @@ -168,22 +213,16 @@ class FriendService { as: 'receiver', attributes: ['id', 'name', 'email'] } - ] + ], + order: [['id', 'ASC']], // 일관된 정렬 순서 추가 + limit, + offset }); - return friends.map(friend => { - const isRequester = friend.requester_id === userId; - const friendInfo = isRequester ? friend.receiver : friend.requester; - - return { - id: friend.id, - status: friend.status, - createdAt: friend.createdAt, - updatedAt: friend.updatedAt, - friendInfo: friendInfo, - relationshipType: isRequester ? 'sent' : 'received' - }; - }); + // 디버깅을 위해 로그 추가 + //console.log(`getFriendList: Retrieved ${friends.length} friends with limit=${limit} and offset=${offset}`); + + return friends.map(friend => new FriendListDTO(friend, userId)); } /** diff --git a/services/friendService.test.js b/services/friendService.test.js new file mode 100644 index 0000000000000000000000000000000000000000..3bb320b64e6fe8cd8395e16212403c0c47085fb0 --- /dev/null +++ b/services/friendService.test.js @@ -0,0 +1,222 @@ +// test/friendService.test.js + +const sequelize = require('../config/sequelize'); // Sequelize 인스턴스 임포트 +const User = require('../models/User'); +const Friend = require('../models/Friend'); +const friendService = require('../services/friendService'); // FriendService 임포트 + +// Sequelize의 Op를 가져오기 위해 추가 +const { Op } = require('sequelize'); + +beforeAll(async () => { + await sequelize.sync({ force: true }); +}); + +beforeEach(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' }, + { id: 3, name: 'Charlie', email: 'charlie@example.com' }, + ]); +}); + +afterAll(async () => { + // 모든 테스트가 끝난 후 데이터베이스 연결을 종료합니다. + await sequelize.close(); +}); + +describe('Friend Service', () => { + describe('validUser', () => { + test('should return user when user exists', async () => { + const user = await friendService.validUser(1); + expect(user).toBeDefined(); + expect(user.name).toBe('Alice'); + }); + + test('should throw error when user does not exist', async () => { + await expect(friendService.validUser(999)).rejects.toThrow('User not found'); + }); + }); + + describe('sendFriendRequest', () => { + test('should send a friend request successfully', async () => { + const friendRequestDTO = await friendService.sendFriendRequest(1, 3); // Alice sends request to Charlie + console.log('sendFriendRequest DTO:', friendRequestDTO); // 디버깅을 위한 로그 추가 + expect(friendRequestDTO).toBeDefined(); + expect(friendRequestDTO.requester.id).toBe(1); + expect(friendRequestDTO.receiver.id).toBe(3); + expect(friendRequestDTO.status).toBe('PENDING'); + }); + + test('should throw error when sending friend request to self', async () => { + await expect(friendService.sendFriendRequest(1, 1)).rejects.toThrow('Cannot send friend request to yourself'); + }); + + test('should throw error when sending duplicate friend request', async () => { + // Alice sends a friend request to Bob + await friendService.sendFriendRequest(1, 2); + // Bob accepts Alice's request + await friendService.acceptFriendRequest(2, 1); + + // Alice tries to send another friend request to Bob + await expect(friendService.sendFriendRequest(1, 2)).rejects.toThrow('Friend request already exists'); + }); + + test('should throw error when user does not exist', async () => { + await expect(friendService.sendFriendRequest(1, 999)).rejects.toThrow('User not found'); + await expect(friendService.sendFriendRequest(999, 1)).rejects.toThrow('User not found'); + }); + }); + + describe('getReceivedRequests', () => { + test('friend requests', async () => { + await friendService.sendFriendRequest(3, 1); + + const receivedRequests = await friendService.getReceivedRequests(1); + expect(receivedRequests.length).toBe(1); + expect(receivedRequests[0].requester.name).toBe('Charlie'); + }); + + test('not send request', async () => { + const receivedRequests = await friendService.getReceivedRequests(2); // Bob has no pending requests + expect(receivedRequests.length).toBe(0); + }); + }); + + describe('getSentRequests', () => { + test('should retrieve sent friend requests', async () => { + await friendService.sendFriendRequest(1, 3); + + const sentRequests = await friendService.getSentRequests(1); + expect(sentRequests.length).toBe(1); + expect(sentRequests[0].receiver.name).toBe('Charlie'); + }); + + test('should return empty array when no sent requests', async () => { + const sentRequests = await friendService.getSentRequests(3); // Charlie has not sent any PENDING requests + expect(sentRequests.length).toBe(0); + }); + }); + + describe('acceptFriendRequest', () => { + test('should accept a pending friend request successfully', async () => { + await friendService.sendFriendRequest(3, 1); + + const updatedRequestDTO = await friendService.acceptFriendRequest(1, 3); + expect(updatedRequestDTO).toBeDefined(); + expect(updatedRequestDTO.status).toBe('ACCEPTED'); + + // Db상태 확인 + const request = await Friend.findOne({ + where: { + requester_id: 3, + receiver_id: 1, + }, + }); + expect(request.status).toBe('ACCEPTED'); + }); + + test('should throw error when accepting non-existing friend request', async () => { + await expect(friendService.acceptFriendRequest(1, 999)).rejects.toThrow('Friend request not found'); + }); + }); + + describe('rejectFriendRequest', () => { + test('should reject a pending friend request successfully', async () => { + await friendService.sendFriendRequest(2, 3); + + const result = await friendService.rejectFriendRequest(3, 2); + expect(result).toBe(1); + + const request = await Friend.findOne({ + where: { + requester_id: 2, + receiver_id: 3, + }, + }); + expect(request).toBeNull(); + }); + + test('should throw error when rejecting non-existing friend request', async () => { + await expect(friendService.rejectFriendRequest(1, 999)).rejects.toThrow('Friend request not found'); + }); + }); + + describe('getFriendList', () => { + test('should retrieve friend list with correct pagination', async () => { + await friendService.sendFriendRequest(1, 2); + await friendService.acceptFriendRequest(2, 1); + + await friendService.sendFriendRequest(1, 3); + await friendService.acceptFriendRequest(3, 1); + + // 추가 더미데이터 생성 + for (let i = 4; i <= 23; i++) { + // Create dummy users + await User.create({ + id: i, + name: `User${i}`, + email: `user${i}@example.com`, + }); + + // Alice랑 친구맺기 + await friendService.sendFriendRequest(1, i); + await friendService.acceptFriendRequest(i, 1); + } + + // Alice 친구: Bob (2), Charlie (3), User4부터 User23까지 (총 22명) + const limit = 5; + const offset = 0; + const friendsPage1 = await friendService.getFriendList(1, limit, offset); + //console.log('getFriendList Page 1:', friendsPage1); // 디버깅을 위한 로그 추가 + expect(friendsPage1.length).toBe(limit); + const expectedNamesPage1 = ['Bob', 'Charlie', 'User4', 'User5', 'User6']; + const receivedNamesPage1 = friendsPage1.map(friend => friend.friendInfo.name); + expectedNamesPage1.forEach(name => { + expect(receivedNamesPage1).toContain(name); + }); + + const friendsPage2 = await friendService.getFriendList(1, limit, limit); + //console.log('getFriendList Page 2:', friendsPage2); // 디버깅을 위한 로그 추가 + expect(friendsPage2.length).toBe(limit); + const expectedNamesPage2 = ['User7', 'User8', 'User9', 'User10', 'User11']; + const receivedNamesPage2 = friendsPage2.map(friend => friend.friendInfo.name); + expectedNamesPage2.forEach(name => { + expect(receivedNamesPage2).toContain(name); + }); + }); + + test('should return empty array when user has no friends', async () => { + const friends = await friendService.getFriendList(999); // Non-existing user + expect(friends.length).toBe(0); + }); + }); + + describe('deleteFriend', () => { + test('should delete an existing friend relationship successfully', async () => { + await friendService.sendFriendRequest(1, 2); + await friendService.acceptFriendRequest(2, 1); + + const result = await friendService.deleteFriend(1, 2); + expect(result).toBe(1); + + const relationship = await Friend.findOne({ + where: { + [Op.or]: [ + { requester_id: 1, receiver_id: 2 }, + { requester_id: 2, receiver_id: 1 }, + ], + status: 'ACCEPTED', + }, + }); + expect(relationship).toBeNull(); + }); + + test('should throw error when deleting a non-existing friend relationship', async () => { + await expect(friendService.deleteFriend(1, 999)).rejects.toThrow('Friend relationship not found'); + }); + }); +}); diff --git a/services/schedule.test.js b/services/schedule.test.js new file mode 100644 index 0000000000000000000000000000000000000000..666dca41fdfdeec3ad6d3d7e23edb7f63e7d55b2 --- /dev/null +++ b/services/schedule.test.js @@ -0,0 +1,244 @@ +// test/schedule.test.js + +const sequelize = require('../config/sequelize'); +const User = require('../models/User'); +const Friend = require('../models/Friend'); +const Schedule = require('../models/Schedule'); +const scheduleService = require('./scheduleService'); // scheduleService 임포트 + +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.create({ + 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, + }); + + await Schedule.create({ + 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'), // 다음 월요일 + }); +}); + +afterAll(async () => { + // 데이터베이스 연결 종료 + await sequelize.close(); +}); + +describe('Schedule Service', () => { + describe('createSchedule', () => { + test('should create a new fixed schedule successfully', async () => { + const scheduleData = { + userId: 2, + title: 'Bob\'s Fixed Schedule', + start_time: new Date('2024-05-03T14:00:00Z'), + end_time: new Date('2024-05-03T15:00:00Z'), + is_fixed: true, + }; + + const schedule = await scheduleService.createSchedule(scheduleData); + + + expect(schedule).toBeDefined(); + expect(schedule.user_id).toBe(2); + expect(schedule.title).toBe('Bob\'s Fixed Schedule'); + expect(schedule.is_fixed).toBe(true); + expect(schedule.expiry_date).toBeNull(); + }); + + test('should create a new flexible schedule with expiry date', async () => { + const scheduleData = { + userId: 2, + title: 'Bob\'s Flexible Schedule', + start_time: new Date('2024-05-04T16:00:00Z'), + end_time: new Date('2024-05-04T17:00:00Z'), + is_fixed: false, + }; + + const schedule = await scheduleService.createSchedule(scheduleData); + + expect(schedule).toBeDefined(); + expect(schedule.user_id).toBe(2); + expect(schedule.title).toBe('Bob\'s Flexible Schedule'); + expect(schedule.is_fixed).toBe(false); + expect(schedule.expiry_date).toBeInstanceOf(Date); + + // expiry_date가 다음 월요일로 설정되었는지 확인 + const expectedExpiryDate = new Date('2024-05-06T00:00:00Z'); // 2024-05-06은 다음 월요일 + expect(schedule.expiry_date.toISOString()).toBe(expectedExpiryDate.toISOString()); + }); + + test('should throw error when schedule times overlap with existing schedule', async () => { + const scheduleData = { + userId: 1, + title: 'Alice\'s Overlapping Schedule', + start_time: new Date('2024-05-01T09:30:00Z'), // 기존 스케줄과 겹침 + end_time: new Date('2024-05-01T10:30:00Z'), + is_fixed: false, + }; + + await expect(scheduleService.createSchedule(scheduleData)).rejects.toThrow('Schedule overlaps with existing schedule'); + }); + + test('should throw error when start_time is after end_time', async () => { + const scheduleData = { + userId: 1, + title: 'Invalid Schedule', + start_time: new Date('2024-05-05T18:00:00Z'), + end_time: new Date('2024-05-05T17:00:00Z'), // start_time이 더 나중 + is_fixed: false, + }; + + await expect(scheduleService.createSchedule(scheduleData)).rejects.toThrow('Start time must be before end time'); + }); + }); + + describe('updateSchedule', () => { + test('should update an existing schedule successfully', async () => { + const updateData = { + title: 'Alice\'s Updated Flexible Schedule', + start_time: new Date('2024-05-02T11:30:00Z'), + end_time: new Date('2024-05-02T12:30:00Z'), + }; + + const updatedSchedule = await scheduleService.updateSchedule(2, 1, updateData); + + expect(updatedSchedule).toBeDefined(); + expect(updatedSchedule.title).toBe('Alice\'s Updated Flexible Schedule'); + expect(updatedSchedule.start_time.toISOString()).toBe(new Date('2024-05-02T11:30:00Z').toISOString()); + expect(updatedSchedule.end_time.toISOString()).toBe(new Date('2024-05-02T12:30:00Z').toISOString()); + expect(updatedSchedule.expiry_date).toBeInstanceOf(Date); + }); + + test('should throw error when updating a non-existing schedule', async () => { + const updateData = { + title: 'Non-existing Schedule', + start_time: new Date('2024-05-06T10:00:00Z'), + end_time: new Date('2024-05-06T11:00:00Z'), + }; + + await expect(scheduleService.updateSchedule(999, 1, updateData)).rejects.toThrow('Schedule not found'); + }); + + test('should throw error when updated schedule overlaps with existing schedule', async () => { + const updateData = { + title: 'Alice\'s Overlapping Update', + start_time: new Date('2024-05-01T09:30:00Z'), // 기존 스케줄과 겹침 + end_time: new Date('2024-05-01T10:30:00Z'), + }; + + await expect(scheduleService.updateSchedule(2, 1, updateData)).rejects.toThrow('Schedule overlaps with existing schedule'); + }); + }); + + describe('deleteSchedule', () => { + test('should delete an existing schedule successfully', async () => { + const result = await scheduleService.deleteSchedule(2, 1); + + expect(result).toEqual({ message: 'Schedule successfully deleted' }); + + // 삭제된 스케줄이 실제로 삭제되었는지 확인 + const schedule = await Schedule.findByPk(2); + expect(schedule).toBeNull(); + }); + + test('should throw error when deleting a non-existing schedule', async () => { + await expect(scheduleService.deleteSchedule(999, 1)).rejects.toThrow('Schedule not found'); + }); +}); + + describe('getAllSchedules', () => { + test('should retrieve all valid schedules for a user', async () => { + // 사용자 Alice의 모든 스케줄 조회 + const schedules = await scheduleService.getAllSchedules(1); + + expect(schedules.length).toBe(1); // id=1 스케줄은 is_fixed=true + expect(schedules[0].title).toBe('Alice\'s Fixed Schedule'); + }); + }); + + describe('getScheduleById', () => { + test('should retrieve a specific schedule by ID', async () => { + const schedule = await scheduleService.getScheduleById(1, 1); + + expect(schedule).toBeDefined(); + expect(schedule.title).toBe('Alice\'s Fixed Schedule'); + }); + + test('should throw error when retrieving a non-existing schedule', async () => { + await expect(scheduleService.getScheduleById(999, 1)).rejects.toThrow('Schedule not found'); + }); + }); + // test/schedule.test.js + + describe('cleanExpiredSchedules', () => { + test('should delete expired flexible schedules', async () => { + // 현재 날짜를 기준으로 만료된 스케줄과 만료되지 않은 스케줄 생성 + const now = new Date('2024-05-07T00:00:00Z'); // 테스트를 위한 고정된 현재 날짜 + + // Jest의 Fake Timers를 사용하여 Date를 고정 + jest.useFakeTimers('modern'); + jest.setSystemTime(now); + + // 만료된 유동 스케줄 생성 + await Schedule.create({ + user_id: 1, + title: 'Expired Flexible Schedule', + start_time: new Date('2024-04-25T10:00:00Z'), + end_time: new Date('2024-04-25T11:00:00Z'), + is_fixed: false, + expiry_date: new Date('2024-05-06T00:00:00Z'), // 이미 만료됨 + }); + + // 만료되지 않은 유동 스케줄 생성 + await Schedule.create({ + user_id: 1, + title: 'Valid Flexible Schedule', + start_time: new Date('2024-05-07T10:00:00Z'), + end_time: new Date('2024-05-07T11:00:00Z'), + is_fixed: false, + expiry_date: new Date('2024-05-14T00:00:00Z'), // 아직 만료되지 않음 + }); + + // 만료된 스케줄 정리 + await scheduleService.cleanExpiredSchedules(); + + // 만료된 스케줄이 삭제되었는지 확인 + const expiredSchedule = await Schedule.findOne({ where: { title: 'Expired Flexible Schedule' } }); + expect(expiredSchedule).toBeNull(); + + // 만료되지 않은 스케줄은 남아있는지 확인 + const validSchedule = await Schedule.findOne({ where: { title: 'Valid Flexible Schedule' } }); + expect(validSchedule).toBeDefined(); + expect(validSchedule.title).toBe('Valid Flexible Schedule'); + + // Jest의 Fake Timers를 복구 + jest.useRealTimers(); + }); +}); + +}); diff --git a/services/scheduleService.js b/services/scheduleService.js index 1cd9d12f49b82e49bb8dd96bad6d11287e3d8831..66d0bbcc3f5d09509632cd564473b2fd14d20c31 100644 --- a/services/scheduleService.js +++ b/services/scheduleService.js @@ -1,240 +1,261 @@ +// services/scheduleService.js + const { Op } = require('sequelize'); const Schedule = require('../models/Schedule'); +const ScheduleResponseDTO = require('../dtos/ScheduleResponseDTO'); class scheduleService { - - /** - * transactin wrapper 함수 - */ - 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; - } + /** + * 트랜잭션 래퍼 함수 + */ + 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; } + } - /** - * 공통 where 절 생성 - */ - getScheduleWhereClause(userId, id = null) { - const where = { - user_id: userId, - [Op.or]: [ - { is_fixed: true }, - { - is_fixed: false, - expiry_date: { - [Op.gt]: new Date() - } - } - ] - }; - - if (id) { - where.id = id; - } - - return where; - } + /** + * 공통 where 절 생성 + */ + getScheduleWhereClause(userId, id = null) { + const where = { + user_id: userId, + [Op.or]: [ + { is_fixed: true }, + { + is_fixed: false, + expiry_date: { [Op.gt]: new Date() }, + }, + ], + }; - /** - * 스케줄 유효성 검사 - */ - validateScheduleTime(start_time, end_time) { - if (new Date(start_time) >= new Date(end_time)) { - throw new Error('Start time must be before end time'); - } + if (id) { + where.id = id; } - /** - * 유동 스케줄 만료일 구하기 - */ - getNextMonday(startTime) { - const date = new Date(startTime); - const day = date.getDay(); - const daysUntilNextMonday = (7 - day + 1) % 7; - - const nextMonday = new Date(date); - nextMonday.setDate(date.getDate() + daysUntilNextMonday); - nextMonday.setHours(0, 0, 0, 0); // 자정으로 설정 - - return nextMonday; - } + return where; + } - /** - * 사용자 스케줄 생성 - */ - async createSchedule({ userId, title, start_time, end_time, is_fixed }) { - return this.withTransaction(async (transaction) => { - this.validateScheduleTime(start_time, end_time); - - const overlap = await this.checkScheduleOverlap(userId, start_time, end_time); - if (overlap) { - throw new Error('Schedule overlaps with existing schedule'); - } - - const scheduleData = { - user_id: userId, - title, - start_time, - end_time, - is_fixed, - expiry_date: is_fixed ? null : this.getNextMonday(start_time) - }; - - return Schedule.create(scheduleData, { transaction }); - }); + /** + * 스케줄 유효성 검사 + * 이미 컨트롤러에서 검증했으므로, 추가 검증 필요 시 수행 + */ + validateScheduleTime(start_time, end_time) { + if (new Date(start_time) >= new Date(end_time)) { + throw new Error("Start time must be before end time"); } + } - /** - * 사용자 스케줄 수정 - */ - async updateSchedule(id, userId, updateData) { - return this.withTransaction(async (transaction) => { - const schedule = await Schedule.findOne({ - where: { id, user_id: userId }, - transaction - }); - - if (!schedule) { - throw new Error('Schedule not found'); - } - - this.validateScheduleTime(updateData.start_time, updateData.end_time); - - const overlap = await this.checkScheduleOverlap( - userId, - updateData.start_time, - updateData.end_time, - id - ); - if (overlap) { - throw new Error('Schedule overlaps with existing schedule'); - } - - const is_fixed = schedule.is_fixed; - const updatedData = { - ...updateData, - expiry_date: is_fixed ? null : this.getNextMonday(updateData.start_time), - updatedAt: new Date() - }; - delete updatedData.is_fixed; - - return schedule.update(updatedData, { transaction }); - }); - } - - /** - * 사용자 스케줄 삭제 - */ - async deleteSchedule(id, userId) { - return this.withTransaction(async (transaction) => { - const result = await Schedule.destroy({ - where: { id, user_id: userId }, - transaction - }); - - if (!result) { - throw new Error('Schedule not found'); - } - - return true; - }); - } - - /** - * 해당 사용자의 스케줄 정보 조회 - */ - async getAllSchedules(userId) { - try { - return Schedule.findAll({ - where: this.getScheduleWhereClause(userId), - order: [['start_time', 'ASC']] - }); - } catch (error) { - throw new Error(`Failed to fetch schedules: ${error.message}`); - } + /** + * 유동 스케줄 만료일 구하기 + */ + getNextMonday(startTime) { + const date = new Date(startTime); + const day = date.getUTCDay(); // 0=Sunday, 1=Monday, ..., 6=Saturday + const daysUntilNextMonday = (8 - day) % 7 || 7; // Ensure next Monday + + const nextMonday = new Date( + Date.UTC( + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate() + daysUntilNextMonday, + 0, + 0, + 0, + 0 // Set to midnight UTC + ) + ); + + return nextMonday; + } + + /** + * 사용자 스케줄 생성 + */ + async createSchedule({ userId, title, start_time, end_time, is_fixed }) { + const schedule = await this.withTransaction(async (transaction) => { + this.validateScheduleTime(start_time, end_time); + + const overlap = await this.checkScheduleOverlap( + userId, + start_time, + end_time + ); + if (overlap) { + throw new Error("Schedule overlaps with existing schedule"); + } + + const scheduleData = { + user_id: userId, + title, + start_time, + end_time, + is_fixed, + expiry_date: is_fixed ? null : this.getNextMonday(start_time), + }; + + return Schedule.create(scheduleData, { transaction }); + }); + + return new ScheduleResponseDTO(schedule); + } + + /** + * 사용자 스케줄 수정 + */ + async updateSchedule(id, userId, updateData) { + const updatedSchedule = await this.withTransaction(async (transaction) => { + const schedule = await Schedule.findOne({ + where: { id, user_id: userId }, + transaction, + }); + + if (!schedule) { + throw new Error("Schedule not found"); + } + + // 이미 컨트롤러에서 검증했으므로, 추가 검증이 필요하다면 수행 + if (updateData.start_time && updateData.end_time) { + this.validateScheduleTime(updateData.start_time, updateData.end_time); + } + + const overlap = await this.checkScheduleOverlap( + userId, + updateData.start_time || schedule.start_time, + updateData.end_time || schedule.end_time, + id + ); + if (overlap) { + throw new Error("Schedule overlaps with existing schedule"); + } + + const is_fixed = schedule.is_fixed; + const updatedDataWithExpiry = { + ...updateData, + expiry_date: is_fixed + ? null + : this.getNextMonday(updateData.start_time || schedule.start_time), + updatedAt: new Date(), + }; + delete updatedDataWithExpiry.is_fixed; + + return schedule.update(updatedDataWithExpiry, { transaction }); + }); + + return new ScheduleResponseDTO(updatedSchedule); + } + + /** + * 사용자 스케줄 삭제 + */ + async deleteSchedule(id, userId) { + return this.withTransaction(async (transaction) => { + const result = await Schedule.destroy({ + where: { id, user_id: userId }, + transaction, + }); + + if (!result) { + throw new Error("Schedule not found"); + } + + // 삭제 성공 메시지 반환 + return { message: "Schedule successfully deleted" }; + }); + } + + /** + * 해당 사용자의 스케줄 정보 조회 + */ + async getAllSchedules(userId) { + try { + const schedules = await Schedule.findAll({ + where: this.getScheduleWhereClause(userId), + order: [["start_time", "ASC"]], + }); + const schedulesDTO = schedules.map( + (schedule) => new ScheduleResponseDTO(schedule) + ); + return schedulesDTO; + } catch (error) { + throw new Error(`Failed to fetch schedules: ${error.message}`); } + } + + /** + * 해당 사용자의 특정 스케줄 조회 + */ + async getScheduleById(id, userId) { + try { + const schedule = await Schedule.findOne({ + where: this.getScheduleWhereClause(userId, id), + }); - /** - * 해당 사용자의 특정 스케줄 조회 - */ - async getScheduleById(id, userId) { - try { - const schedule = await Schedule.findOne({ - where: this.getScheduleWhereClause(userId, id) - }); - - if (!schedule) { - throw new Error('Schedule not found'); - } - - return schedule; - } catch (error) { - throw new Error(`Failed to fetch schedule: ${error.message}`); - } + if (!schedule) { + throw new Error("Schedule not found"); + } + + return new ScheduleResponseDTO(schedule); + } catch (error) { + throw new Error(`Failed to fetch schedule: ${error.message}`); } - - - /** - * 만료된 유동 스케줄 정리 -> utils에 cron job 추가해서 실행하도록 설정 - */ - async cleanExpiredSchedules() { - try { - await Schedule.destroy({ - where: { - is_fixed: false, - expiry_date: { - [Op.lte]: new Date() - } - } - }); - } catch (error) { - throw new Error(`Failed to clean expired schedules: ${error.message}`); - } + } + + /** + * 만료된 유동 스케줄 정리 + */ + async cleanExpiredSchedules() { + try { + await Schedule.destroy({ + where: { + is_fixed: false, + expiry_date: { [Op.lte]: new Date() }, + }, + }); + } catch (error) { + throw new Error(`Failed to clean expired schedules: ${error.message}`); } + } + + /** + * 스케줄 중복 검사 + */ + async checkScheduleOverlap(userId, start_time, end_time, excludeId = null) { + try { + const where = { + user_id: userId, + [Op.or]: [ + { + [Op.and]: [ + { start_time: { [Op.lte]: start_time } }, + { end_time: { [Op.gte]: start_time } }, + ], + }, + { + [Op.and]: [ + { start_time: { [Op.gte]: start_time } }, + { start_time: { [Op.lte]: end_time } }, + ], + }, + ], + }; + if (excludeId) { + where.id = { [Op.ne]: excludeId }; + } - /** - * 스케줄 중복 검사 -> 기존 스케줄 시간대에 추가 못하도록 - */ - async checkScheduleOverlap(userId, start_time, end_time, excludeId = null) { - try { - const where = { - user_id: userId, - [Op.or]: [ - { - // 새로운 스케줄이 기존 스케줄 내 존재 - [Op.and]: [ - { start_time: { [Op.lte]: start_time } }, - { end_time: { [Op.gte]: start_time } } - ] - }, - { - // 새로운 스케줄이 기존 스케줄을 포함 - [Op.and]: [ - { start_time: { [Op.gte]: start_time } }, - { start_time: { [Op.lte]: end_time } } - ] - } - ] - }; - - if (excludeId) { - where.id = { [Op.ne]: excludeId }; - } - - const overlappingSchedule = await Schedule.findOne({ where }); - return overlappingSchedule; - } catch (error) { - throw new Error(`Failed to check schedule overlap: ${error.message}`); - } + const overlappingSchedule = await Schedule.findOne({ where }); + return overlappingSchedule; + } catch (error) { + throw new Error(`Failed to check schedule overlap: ${error.message}`); } + } } module.exports = new scheduleService(); diff --git a/test/friendService.test.js b/test/friendService.test.js index ca18666c14fb343a97b406a618e1b481595a09a8..3bb320b64e6fe8cd8395e16212403c0c47085fb0 100644 --- a/test/friendService.test.js +++ b/test/friendService.test.js @@ -9,212 +9,214 @@ const friendService = require('../services/friendService'); // FriendService 임 const { Op } = require('sequelize'); beforeAll(async () => { - // 테스트 전에 데이터베이스를 동기화하고 테이블을 생성합니다. - await sequelize.sync({ force: true }); + await sequelize.sync({ force: true }); }); beforeEach(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' }, - { id: 3, name: 'Charlie', email: 'charlie@example.com' }, - ]); + await sequelize.sync({ force: true }); + + // 더미 사용자 생성 + await User.bulkCreate([ + { id: 1, name: 'Alice', email: 'alice@example.com' }, + { id: 2, name: 'Bob', email: 'bob@example.com' }, + { id: 3, name: 'Charlie', email: 'charlie@example.com' }, + ]); }); afterAll(async () => { - // 모든 테스트가 끝난 후 데이터베이스 연결을 종료합니다. - await sequelize.close(); + // 모든 테스트가 끝난 후 데이터베이스 연결을 종료합니다. + await sequelize.close(); }); describe('Friend Service', () => { - describe('validUser', () => { - test('should return user when user exists', async () => { - const user = await friendService.validUser(1); - expect(user).toBeDefined(); - expect(user.name).toBe('Alice'); - }); + describe('validUser', () => { + test('should return user when user exists', async () => { + const user = await friendService.validUser(1); + expect(user).toBeDefined(); + expect(user.name).toBe('Alice'); + }); - test('should throw error when user does not exist', async () => { - await expect(friendService.validUser(999)).rejects.toThrow('User not found'); - }); - }); - - describe('sendFriendRequest', () => { - test('should send a friend request successfully', async () => { - const friendRequest = await friendService.sendFriendRequest(1, 3); // Alice sends request to Charlie - expect(friendRequest).toBeDefined(); - expect(friendRequest.requester_id).toBe(1); - expect(friendRequest.receiver_id).toBe(3); - expect(friendRequest.status).toBe('PENDING'); + test('should throw error when user does not exist', async () => { + await expect(friendService.validUser(999)).rejects.toThrow('User not found'); + }); }); - test('should throw error when sending friend request to self', async () => { - await expect(friendService.sendFriendRequest(1, 1)).rejects.toThrow('Cannot send friend request to yourself'); - }); + describe('sendFriendRequest', () => { + test('should send a friend request successfully', async () => { + const friendRequestDTO = await friendService.sendFriendRequest(1, 3); // Alice sends request to Charlie + console.log('sendFriendRequest DTO:', friendRequestDTO); // 디버깅을 위한 로그 추가 + expect(friendRequestDTO).toBeDefined(); + expect(friendRequestDTO.requester.id).toBe(1); + expect(friendRequestDTO.receiver.id).toBe(3); + expect(friendRequestDTO.status).toBe('PENDING'); + }); - test('should throw error when sending duplicate friend request', async () => { - - await friendService.sendFriendRequest(1, 2); + test('should throw error when sending friend request to self', async () => { + await expect(friendService.sendFriendRequest(1, 1)).rejects.toThrow('Cannot send friend request to yourself'); + }); - - await friendService.acceptFriendRequest(2, 1); + test('should throw error when sending duplicate friend request', async () => { + // Alice sends a friend request to Bob + await friendService.sendFriendRequest(1, 2); + // Bob accepts Alice's request + await friendService.acceptFriendRequest(2, 1); - await expect(friendService.sendFriendRequest(1, 2)).rejects.toThrow('Friend request already exists'); - }); + // Alice tries to send another friend request to Bob + await expect(friendService.sendFriendRequest(1, 2)).rejects.toThrow('Friend request already exists'); + }); - test('should throw error when user does not exist', async () => { - await expect(friendService.sendFriendRequest(1, 999)).rejects.toThrow('User not found'); - await expect(friendService.sendFriendRequest(999, 1)).rejects.toThrow('User not found'); + test('should throw error when user does not exist', async () => { + await expect(friendService.sendFriendRequest(1, 999)).rejects.toThrow('User not found'); + await expect(friendService.sendFriendRequest(999, 1)).rejects.toThrow('User not found'); + }); }); - }); - describe('getReceivedRequests', () => { - test('friend requests', async () => { - await friendService.sendFriendRequest(3, 1); + describe('getReceivedRequests', () => { + test('friend requests', async () => { + await friendService.sendFriendRequest(3, 1); - const receivedRequests = await friendService.getReceivedRequests(1); - expect(receivedRequests.length).toBe(1); - expect(receivedRequests[0].requester.name).toBe('Charlie'); - }); + const receivedRequests = await friendService.getReceivedRequests(1); + expect(receivedRequests.length).toBe(1); + expect(receivedRequests[0].requester.name).toBe('Charlie'); + }); - test('not send request', async () => { - const receivedRequests = await friendService.getReceivedRequests(2); - expect(receivedRequests.length).toBe(0); + test('not send request', async () => { + const receivedRequests = await friendService.getReceivedRequests(2); // Bob has no pending requests + expect(receivedRequests.length).toBe(0); + }); }); - }); - describe('getSentRequests', () => { - test('should retrieve sent friend requests', async () => { + describe('getSentRequests', () => { + test('should retrieve sent friend requests', async () => { + await friendService.sendFriendRequest(1, 3); - await friendService.sendFriendRequest(1, 3); + const sentRequests = await friendService.getSentRequests(1); + expect(sentRequests.length).toBe(1); + expect(sentRequests[0].receiver.name).toBe('Charlie'); + }); - const sentRequests = await friendService.getSentRequests(1); - expect(sentRequests.length).toBe(1); - expect(sentRequests[0].receiver.name).toBe('Charlie'); + test('should return empty array when no sent requests', async () => { + const sentRequests = await friendService.getSentRequests(3); // Charlie has not sent any PENDING requests + expect(sentRequests.length).toBe(0); + }); }); - test('should return empty array when no sent requests', async () => { - const sentRequests = await friendService.getSentRequests(3); // Charlie has not sent any PENDING requests - expect(sentRequests.length).toBe(0); - }); - }); - - describe('acceptFriendRequest', () => { - test('should accept a pending friend request successfully', async () => { - - await friendService.sendFriendRequest(3, 1); - - const updatedRequest = await friendService.acceptFriendRequest(1, 3); - expect(updatedRequest).toBeDefined(); - expect(updatedRequest.status).toBe('ACCEPTED'); - - //Db상태 확인 - const request = await Friend.findOne({ - where: { - requester_id: 3, - receiver_id: 1, - }, - }); - expect(request.status).toBe('ACCEPTED'); - }); + describe('acceptFriendRequest', () => { + test('should accept a pending friend request successfully', async () => { + await friendService.sendFriendRequest(3, 1); - test('없는 요청수락', async () => { - await expect(friendService.acceptFriendRequest(1, 999)).rejects.toThrow('Friend request not found'); - }); - }); - - describe('rejectFriendRequest', () => { - test('should reject a pending friend request successfully', async () => { - - await friendService.sendFriendRequest(2, 3); - - - const result = await friendService.rejectFriendRequest(3, 2); - expect(result).toBe(1); - - const request = await Friend.findOne({ - where: { - requester_id: 2, - receiver_id: 3, - }, - }); - expect(request).toBeNull(); - }); + const updatedRequestDTO = await friendService.acceptFriendRequest(1, 3); + expect(updatedRequestDTO).toBeDefined(); + expect(updatedRequestDTO.status).toBe('ACCEPTED'); - test('should throw error when rejecting non-existing friend request', async () => { - await expect(friendService.rejectFriendRequest(1, 999)).rejects.toThrow('Friend request not found'); - }); - }); - - describe('getFriendList', () => { - test('should retrieve friend list with correct pagination', async () => { - await friendService.sendFriendRequest(1, 2); - await friendService.acceptFriendRequest(2, 1); - - await friendService.sendFriendRequest(1, 3); - await friendService.acceptFriendRequest(3, 1); - - // 추가 더미데이터 생성 - for (let i = 4; i <= 23; i++) { - // Create dummy users - await User.create({ - id: i, - name: `User${i}`, - email: `user${i}@example.com`, - }); - - //Alice랑 친구맺기 - await friendService.sendFriendRequest(1, i); - await friendService.acceptFriendRequest(i, 1); - } - - // Alice 친구: Bob (2), Charlie (3),User4 to User23 (20 friends) - const limit = 5; - const offset = 0; - const friendsPage1 = await friendService.getFriendList(1, limit, offset); - expect(friendsPage1.length).toBe(limit); - expect(['Bob', 'Charlie', 'User4', 'User5', 'User6']).toContain(friendsPage1[0].friendInfo.name); - - const friendsPage2 = await friendService.getFriendList(1, limit, limit); - expect(friendsPage2.length).toBe(limit); - expect(['User7', 'User8', 'User9', 'User10', 'User11']).toContain(friendsPage2[0].friendInfo.name); + // Db상태 확인 + const request = await Friend.findOne({ + where: { + requester_id: 3, + receiver_id: 1, + }, + }); + expect(request.status).toBe('ACCEPTED'); + }); + + test('should throw error when accepting non-existing friend request', async () => { + await expect(friendService.acceptFriendRequest(1, 999)).rejects.toThrow('Friend request not found'); + }); }); - - test('should return empty array when user has no friends', async () => { - const friends = await friendService.getFriendList(999); // Non-existing user - expect(friends.length).toBe(0); + + describe('rejectFriendRequest', () => { + test('should reject a pending friend request successfully', async () => { + await friendService.sendFriendRequest(2, 3); + + const result = await friendService.rejectFriendRequest(3, 2); + expect(result).toBe(1); + + const request = await Friend.findOne({ + where: { + requester_id: 2, + receiver_id: 3, + }, + }); + expect(request).toBeNull(); + }); + + test('should throw error when rejecting non-existing friend request', async () => { + await expect(friendService.rejectFriendRequest(1, 999)).rejects.toThrow('Friend request not found'); + }); }); - }); - - describe('deleteFriend', () => { - test('should delete an existing friend relationship successfully', async () => { - - await friendService.sendFriendRequest(1, 2); - await friendService.acceptFriendRequest(2, 1); - - - const result = await friendService.deleteFriend(1, 2); - expect(result).toBe(1); - - - const relationship = await Friend.findOne({ - where: { - [Op.or]: [ - { requester_id: 1, receiver_id: 2 }, - { requester_id: 2, receiver_id: 1 }, - ], - status: 'ACCEPTED', - }, - }); - expect(relationship).toBeNull(); + + describe('getFriendList', () => { + test('should retrieve friend list with correct pagination', async () => { + await friendService.sendFriendRequest(1, 2); + await friendService.acceptFriendRequest(2, 1); + + await friendService.sendFriendRequest(1, 3); + await friendService.acceptFriendRequest(3, 1); + + // 추가 더미데이터 생성 + for (let i = 4; i <= 23; i++) { + // Create dummy users + await User.create({ + id: i, + name: `User${i}`, + email: `user${i}@example.com`, + }); + + // Alice랑 친구맺기 + await friendService.sendFriendRequest(1, i); + await friendService.acceptFriendRequest(i, 1); + } + + // Alice 친구: Bob (2), Charlie (3), User4부터 User23까지 (총 22명) + const limit = 5; + const offset = 0; + const friendsPage1 = await friendService.getFriendList(1, limit, offset); + //console.log('getFriendList Page 1:', friendsPage1); // 디버깅을 위한 로그 추가 + expect(friendsPage1.length).toBe(limit); + const expectedNamesPage1 = ['Bob', 'Charlie', 'User4', 'User5', 'User6']; + const receivedNamesPage1 = friendsPage1.map(friend => friend.friendInfo.name); + expectedNamesPage1.forEach(name => { + expect(receivedNamesPage1).toContain(name); + }); + + const friendsPage2 = await friendService.getFriendList(1, limit, limit); + //console.log('getFriendList Page 2:', friendsPage2); // 디버깅을 위한 로그 추가 + expect(friendsPage2.length).toBe(limit); + const expectedNamesPage2 = ['User7', 'User8', 'User9', 'User10', 'User11']; + const receivedNamesPage2 = friendsPage2.map(friend => friend.friendInfo.name); + expectedNamesPage2.forEach(name => { + expect(receivedNamesPage2).toContain(name); + }); + }); + + test('should return empty array when user has no friends', async () => { + const friends = await friendService.getFriendList(999); // Non-existing user + expect(friends.length).toBe(0); + }); }); - test('should throw error when deleting a non-existing friend relationship', async () => { - await expect(friendService.deleteFriend(1, 999)).rejects.toThrow('Friend relationship not found'); + describe('deleteFriend', () => { + test('should delete an existing friend relationship successfully', async () => { + await friendService.sendFriendRequest(1, 2); + await friendService.acceptFriendRequest(2, 1); + + const result = await friendService.deleteFriend(1, 2); + expect(result).toBe(1); + + const relationship = await Friend.findOne({ + where: { + [Op.or]: [ + { requester_id: 1, receiver_id: 2 }, + { requester_id: 2, receiver_id: 1 }, + ], + status: 'ACCEPTED', + }, + }); + expect(relationship).toBeNull(); + }); + + test('should throw error when deleting a non-existing friend relationship', async () => { + await expect(friendService.deleteFriend(1, 999)).rejects.toThrow('Friend relationship not found'); + }); }); - }); }); diff --git a/test/friendrelation.test.js b/test/friendrelation.test.js index 04a2601b50d9554dcdeba6065b3673a6fd5c3205..4c5c1ede7371f09d7b6dc5a47f05f5b94e9d0dd5 100644 --- a/test/friendrelation.test.js +++ b/test/friendrelation.test.js @@ -94,17 +94,6 @@ describe('User and Friend Relationships', () => { expect(bob.sentRequests[0].receiver.name).toBe('Alice'); }); - test('self friend reqeust', async () => { - await expect( - Friend.create({ - id: 3, - requester_id: 1, - receiver_id: 1, // 자신에게 요청 - status: 'PENDING', - }) - ).rejects.toThrow(); - }); - test('already request test', async () => { // Alice가 Bob에게 이미 친구 요청을 보냈으므로, 다시 보내면 에러 발생 await expect(