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

refactor: 연관관계 수정(#12)

parent 7ea23de0
No related branches found
No related tags found
2 merge requests!31Develop,!19미팅방 서비스 관련 검증 로직 보강 및 트랜잭션 적용
Showing
with 11453 additions and 7070 deletions
...@@ -4,7 +4,7 @@ require('dotenv').config(); ...@@ -4,7 +4,7 @@ require('dotenv').config();
const express = require('express'); const express = require('express');
const session = require('express-session'); const session = require('express-session');
const passport = require('./passport'); // 변경된 경로 const passport = require('./passport');
const flash = require('connect-flash'); const flash = require('connect-flash');
const { initScheduleCleaner } = require('./utils/scheduler'); const { initScheduleCleaner } = require('./utils/scheduler');
const connectMongoDB = require('./config/mongoose'); // MongoDB 연결 const connectMongoDB = require('./config/mongoose'); // MongoDB 연결
...@@ -16,10 +16,10 @@ const app = express(); ...@@ -16,10 +16,10 @@ const app = express();
// CORS 설정 // CORS 설정
app.use( app.use(
cors({ cors({
origin: 'http://localhost:3000', // 허용할 도메인 설정 (예: 프론트엔드 주소) origin: 'http://localhost:3000',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'], allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // 쿠키와 인증 정보를 허용하려면 true로 설정 credentials: true,
}) })
); );
...@@ -41,12 +41,10 @@ app.use( ...@@ -41,12 +41,10 @@ app.use(
app.use(passport.initialize()); app.use(passport.initialize());
app.use(passport.session()); app.use(passport.session());
// 플래시 메시지 (선택 사항)
app.use(flash()); app.use(flash());
/** //라우터 등록
* 라우터 등록
*/
const authRoutes = require('./routes/auth'); const authRoutes = require('./routes/auth');
app.use('/auth', authRoutes); app.use('/auth', authRoutes);
...@@ -59,7 +57,7 @@ app.use('/api/friend', friendRoutes); ...@@ -59,7 +57,7 @@ app.use('/api/friend', friendRoutes);
const meetingRoutes = require('./routes/meetingRoute'); const meetingRoutes = require('./routes/meetingRoute');
app.use('/api/meeting', meetingRoutes); app.use('/api/meeting', meetingRoutes);
const chatRoutes = require('./routes/chatRoute'); //const chatRoutes = require('./routes/chatRoute');
app.use('/api/chat', chatRoutes); app.use('/api/chat', chatRoutes);
// 스케줄 클리너 초기화 // 스케줄 클리너 초기화
...@@ -72,11 +70,11 @@ const PORT = process.env.PORT || 3000; ...@@ -72,11 +70,11 @@ const PORT = process.env.PORT || 3000;
try { try {
// MongoDB 연결 // MongoDB 연결
await connectMongoDB(); await connectMongoDB();
console.log('✅ MongoDB 연결 성공'); //console.log('✅ MongoDB 연결 성공');
// MySQL 연결 확인 // MySQL 연결 확인
await sequelize.authenticate(); await sequelize.authenticate();
console.log('✅ MySQL 연결 성공'); //console.log('✅ MySQL 연결 성공');
// 서버 시작 // 서버 시작
app.listen(PORT, () => { app.listen(PORT, () => {
......
// config/sequelize.js // src/config/sequelize.js
const { Sequelize } = require('sequelize'); const { Sequelize } = require('sequelize');
require('dotenv').config();
const sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASSWORD, { const isTest = process.env.NODE_ENV === 'test';
host: process.env.DB_HOST,
dialect: 'mysql', // 사용하려는 DBMS에 맞게 변경 const sequelize = isTest
logging: false, ? new Sequelize('sqlite::memory:', { logging: false }) // 테스트 환경용 인메모리 DB
define: { : new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASSWORD, {
//timestamps: true, // createdAt, updatedAt 자동 생성 host: process.env.DB_HOST,
underscored: true, // created_at 형식의 필드명 사용 dialect: 'mysql',
}, logging: false,
}); });
module.exports = sequelize; module.exports = sequelize;
\ No newline at end of file
// controllers/scheduleController.js
const ScheduleService = require('../services/scheduleService'); const ScheduleService = require('../services/scheduleService');
const ScheduleRequestDTO = require('../dtos/ScheduleRequestDTO');
class scheduleController { class scheduleController {
/** /**
* 스케줄 생성 * 스케줄 생성
* POST /api/schedule * POST /api/schedule
* 해당 사용자 id는 auth 미들웨어에서 설정된 사용자 정보 이용
* req.user = User 모델의 인스턴스
*/ */
async createSchedule(req, res) { async createSchedule(req, res) {
try { try {
const userId = req.user.id; const userId = req.user.id;
const { title, start_time, end_time, is_fixed } = req.body; const scheduleRequestDTO = new ScheduleRequestDTO(req.body);
const validatedData = scheduleRequestDTO.validate('create');
const schedule = await ScheduleService.createSchedule({ const scheduleDTO = await ScheduleService.createSchedule({
userId, userId,
title, ...validatedData
start_time,
end_time,
is_fixed
}); });
return res.status(201).json({ return res.status(201).json({
success: true, success: true,
data: { data: scheduleDTO
schedule
}
}); });
} catch (error) { } catch (error) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
error: { error: {
message: error.message, message: error.message.includes('Validation error') ? error.message : 'SCHEDULE_CREATE_ERROR',
code: 'SCHEDULE_CREATE_ERROR' code: error.message.includes('Validation error') ? 'VALIDATION_ERROR' : 'SCHEDULE_CREATE_ERROR'
} }
}); });
} }
...@@ -44,19 +40,15 @@ class scheduleController { ...@@ -44,19 +40,15 @@ class scheduleController {
async updateSchedule(req, res) { async updateSchedule(req, res) {
try { try {
const { id } = req.params; const { id } = req.params;
const { title, start_time, end_time } = req.body;
const userId = req.user.id; const userId = req.user.id;
const schedule = await ScheduleService.updateSchedule(id, userId, const scheduleRequestDTO = new ScheduleRequestDTO(req.body);
{ const validatedData = scheduleRequestDTO.validate('update');
title,
start_time, const scheduleDTO = await ScheduleService.updateSchedule(id, userId, validatedData);
end_time
});
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
data: schedule data: scheduleDTO
}); });
} catch (error) { } catch (error) {
if (error.message === 'Schedule not found') { if (error.message === 'Schedule not found') {
...@@ -67,6 +59,14 @@ class scheduleController { ...@@ -67,6 +59,14 @@ class scheduleController {
code: 'SCHEDULE_NOT_FOUND' code: 'SCHEDULE_NOT_FOUND'
} }
}); });
} else if (error.message.includes('Validation error')) {
return res.status(400).json({
success: false,
error: {
message: error.message,
code: 'VALIDATION_ERROR'
}
});
} }
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
...@@ -87,20 +87,27 @@ class scheduleController { ...@@ -87,20 +87,27 @@ class scheduleController {
const { id } = req.params; const { id } = req.params;
const userId = req.user.id; const userId = req.user.id;
await ScheduleService.deleteSchedule(id, userId); const deleteResult = await ScheduleService.deleteSchedule(id, userId);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
data: { data: deleteResult
message: 'Schedule successfully deleted'
}
}); });
} catch (error) { } catch (error) {
return res.status(404).json({ if (error.message === 'Schedule not found') {
return res.status(404).json({
success: false,
error: {
message: error.message,
code: 'SCHEDULE_NOT_FOUND'
}
});
}
return res.status(500).json({
success: false, success: false,
error: { error: {
message: error.message, message: 'Failed to delete schedule',
code: 'SCHEDULE_NOT_FOUND' code: 'DELETE_ERROR'
} }
}); });
} }
...@@ -113,11 +120,11 @@ class scheduleController { ...@@ -113,11 +120,11 @@ class scheduleController {
async getAllSchedules(req, res) { async getAllSchedules(req, res) {
try { try {
const userId = req.user.id; const userId = req.user.id;
const schedules = await ScheduleService.getAllSchedules(userId); const schedulesDTO = await ScheduleService.getAllSchedules(userId);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
data: schedules data: schedulesDTO
}); });
} catch (error) { } catch (error) {
return res.status(500).json({ return res.status(500).json({
...@@ -138,11 +145,11 @@ class scheduleController { ...@@ -138,11 +145,11 @@ class scheduleController {
try { try {
const { id } = req.params; const { id } = req.params;
const userId = req.user.id; const userId = req.user.id;
const schedule = await ScheduleService.getScheduleById(id, userId); const scheduleDTO = await ScheduleService.getScheduleById(id, userId);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
data: schedule data: scheduleDTO
}); });
} catch (error) { } catch (error) {
if (error.message === 'Schedule not found') { if (error.message === 'Schedule not found') {
...@@ -166,4 +173,4 @@ class scheduleController { ...@@ -166,4 +173,4 @@ class scheduleController {
} }
} }
module.exports = new scheduleController(); module.exports = new scheduleController();
\ No newline at end of file
// dto/FriendListDTO.js
class FriendListDTO {
/**
* @param {object} friend - Friend relationship object retrieved from the database.
* @param {number} userId - The ID of the user whose friend list is being retrieved.
*/
constructor(friend, userId) {
this.id = friend.id;
this.status = friend.status;
this.createdAt = friend.createdAt;
this.updatedAt = friend.updatedAt;
this.friendInfo = friend.requester_id === userId ? {
id: friend.receiver.id,
name: friend.receiver.name,
email: friend.receiver.email
} : {
id: friend.requester.id,
name: friend.requester.name,
email: friend.requester.email
};
this.relationshipType = friend.requester_id === userId ? 'sent' : 'received';
}
}
module.exports = FriendListDTO;
// dto/FriendRequestDTO.js
class FriendRequestDTO {
/**
* @param {object} friendRequest - Friend request object retrieved from the database.
*/
constructor(friendRequest) {
this.id = friendRequest.id;
this.requester = {
id: friendRequest.requester.id,
name: friendRequest.requester.name,
email: friendRequest.requester.email
};
this.receiver = {
id: friendRequest.receiver.id,
name: friendRequest.receiver.name,
email: friendRequest.receiver.email
};
this.status = friendRequest.status;
this.createdAt = friendRequest.createdAt;
this.updatedAt = friendRequest.updatedAt;
}
}
module.exports = FriendRequestDTO;
File moved
File moved
// dtos/ScheduleRequestDTO.js
const Joi = require('joi');
class ScheduleRequestDTO {
constructor(data) {
this.data = data;
}
validate(type = 'create') {
// 기본 스키마 정의
let schema = Joi.object({
title: Joi.string().min(1).max(255).required(),
start_time: Joi.date().iso().required(),
end_time: Joi.date().iso().required(),
is_fixed: Joi.boolean().required()
});
// 'update' 타입의 경우 모든 필드를 필수로 하지 않을 수 있음
if (type === 'update') {
schema = Joi.object({
title: Joi.string().min(1).max(255).optional(),
start_time: Joi.date().iso().optional(),
end_time: Joi.date().iso().optional(),
is_fixed: Joi.boolean().optional()
}).or('title', 'start_time', 'end_time', 'is_fixed'); // 최소 한 개 이상의 필드가 필요
}
const { error, value } = schema.validate(this.data, { abortEarly: false });
if (error) {
// 모든 에러 메시지를 하나의 문자열로 결합
const errorMessages = error.details.map(detail => detail.message).join(', ');
throw new Error(`Validation error: ${errorMessages}`);
}
// 검증된 데이터를 반환
return value;
}
}
module.exports = ScheduleRequestDTO;
// dtos/ScheduleResponseDTO.js
class ScheduleResponseDTO {
constructor(schedule) {
this.id = schedule.id;
this.user_id = schedule.user_id;
this.title = schedule.title;
this.start_time = schedule.start_time;
this.end_time = schedule.end_time;
this.is_fixed = schedule.is_fixed;
this.expiry_date = schedule.expiry_date;
this.createdAt = schedule.createdAt;
this.updatedAt = schedule.updatedAt;
}
}
module.exports = ScheduleResponseDTO;
...@@ -8,11 +8,12 @@ const Friend = sequelize.define('Friend', { ...@@ -8,11 +8,12 @@ const Friend = sequelize.define('Friend', {
status: { status: {
type: DataTypes.ENUM('PENDING', 'ACCEPTED'), type: DataTypes.ENUM('PENDING', 'ACCEPTED'),
allowNull: false, allowNull: false,
defaultValue: 'PENDING' defaultValue: 'PENDING',
} }
}, { }, {
tableName: 'Friends', tableName: 'Friends',
timestamps: true, timestamps: true,
underscored: true,
indexes: [ indexes: [
{ {
unique: true, unique: true,
...@@ -28,7 +29,7 @@ const Friend = sequelize.define('Friend', { ...@@ -28,7 +29,7 @@ const Friend = sequelize.define('Friend', {
Friend.belongsTo(User, { foreignKey: 'requester_id', as: 'requester' }); // 친구 요청을 보낸 사용자 Friend.belongsTo(User, { foreignKey: 'requester_id', as: 'requester' }); // 친구 요청을 보낸 사용자
Friend.belongsTo(User, { foreignKey: 'receiver_id', as: 'receiver' }); // 친구 요청을 받은 사용자 Friend.belongsTo(User, { foreignKey: 'receiver_id', as: 'receiver' }); // 친구 요청을 받은 사용자
User.hasMany(Friend, { foreignKey: 'requester_id', as: 'sentRequests' }); // 친구 요청을 보낸 목록 User.hasMany(Friend, { foreignKey: 'requester_id', as: 'sentRequests' }); // 친구 요청을 보낸 목록
User.hasMany(Friend, { foreignKey: 'receiver_id', as: 'receivedRequests' }); // 친구 요청을 받은 목록 User.hasMany(Friend, { foreignKey: 'receiver_id', as: 'receivedRequests' }); // 친구 요청을 받은 목록
module.exports = Friend; module.exports = Friend;
// models/Meeting.js // models/Meeting.js
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/sequelize'); const sequelize = require('../config/sequelize');
const User = require('./User'); const User = require('./User');
const Meeting = sequelize.define('Meeting', { const Meeting = sequelize.define('Meeting', {
id: {
type: DataTypes.BIGINT,
primaryKey: true,
autoIncrement: true,
},
title: { title: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
...@@ -36,12 +31,8 @@ const Meeting = sequelize.define('Meeting', { ...@@ -36,12 +31,8 @@ const Meeting = sequelize.define('Meeting', {
allowNull: false, allowNull: false,
}, },
created_by: { created_by: {
type: DataTypes.BIGINT, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
references: {
model: 'Users',
key: 'id',
},
}, },
chatRoomId: { // 새로운 필드 추가 chatRoomId: { // 새로운 필드 추가
type: DataTypes.STRING, type: DataTypes.STRING,
...@@ -54,15 +45,8 @@ const Meeting = sequelize.define('Meeting', { ...@@ -54,15 +45,8 @@ const Meeting = sequelize.define('Meeting', {
// 연관 관계 설정 // 연관 관계 설정
Meeting.associate = (models) => { Meeting.belongsTo(User, { foreignKey: 'created_by', as: 'creator' });
Meeting.belongsTo(models.User, { User.hasMany(Meeting, { foreignKey: 'created_by', as: 'meetings' });
foreignKey: 'created_by', // FK 설정
as: 'creator', // 별칭 module.exports = Meeting;
});
Meeting.hasMany(models.MeetingParticipant, {
foreignKey: 'meeting_id',
as: 'participants',
});
};
module.exports = Meeting;
\ No newline at end of file
// models/MeetingParticipant.js // models/MeetingParticipant.js
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/sequelize'); const sequelize = require('../config/sequelize');
const Meeting =require('./Meeting');
const User = require('./User');
const MeetingParticipant = sequelize.define('MeetingParticipant', { const MeetingParticipant = sequelize.define('MeetingParticipant', {
meeting_id: { meeting_id: {
type: DataTypes.BIGINT, type: DataTypes.INTEGER,
allowNull: false allowNull: false
}, },
user_id: { user_id: {
type: DataTypes.BIGINT, type: DataTypes.INTEGER,
allowNull: false allowNull: false
} }
}, { }, {
...@@ -17,16 +20,11 @@ const MeetingParticipant = sequelize.define('MeetingParticipant', { ...@@ -17,16 +20,11 @@ const MeetingParticipant = sequelize.define('MeetingParticipant', {
timestamps: false, timestamps: false,
}); });
MeetingParticipant.associate = (models) => { MeetingParticipant.belongsTo(Meeting, { foreignKey: 'meeting_id', as: 'meeting' });
MeetingParticipant.belongsTo(models.Meeting, { Meeting.hasMany(MeetingParticipant, { foreignKey: 'meeting_id', as: 'participants' });
foreignKey: 'meeting_id',
as: 'meeting' MeetingParticipant.belongsTo(User, { foreignKey: 'user_id', as: 'user' });
}); User.hasMany(MeetingParticipant, { foreignKey: 'user_id', as: 'meetingParticipations' });
MeetingParticipant.belongsTo(models.User, {
foreignKey: 'user_id',
as: 'participantUser'
});
};
module.exports = MeetingParticipant; module.exports = MeetingParticipant;
// models/User.js // models/User.js
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/sequelize'); const sequelize = require('../config/sequelize');
const User = sequelize.define('User', { const User = sequelize.define('User', {
id: { name: {
type: DataTypes.BIGINT, // 수정: id 필드를 BIGINT로 설정 type: DataTypes.STRING, // VARCHAR
autoIncrement: true, allowNull: false,
primaryKey: true, },
}, email: {
name: { type: DataTypes.STRING, // VARCHAR
type: DataTypes.STRING, // VARCHAR allowNull: false,
allowNull: false, unique: true,
}, validate: {
email: { isEmail: true,
type: DataTypes.STRING, // VARCHAR },
allowNull: false,
unique: true,
validate: {
isEmail: true,
}, },
},
}, { }, {
tableName: 'Users', tableName: 'Users',
timestamps: true, // createdAt과 updatedAt 자동 관리 timestamps: true, // createdAt과 updatedAt 자동 관리
}); });
User.associate = (models) => {
User.hasMany(models.Meeting, {
foreignKey: 'created_by',
as: 'createdMeetings',
});
User.hasMany(models.MeetingParticipant, {
foreignKey: 'user_id',
as: 'userMeetingParticipations',
});
};
module.exports = User; module.exports = User;
Source diff could not be displayed: it is too large. Options to address this: view the blob.
...@@ -3,8 +3,7 @@ ...@@ -3,8 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"main": "app.js", "main": "app.js",
"scripts": { "scripts": {
"start": "nodemon app", "test": "cross-env NODE_ENV=test jest"
"test": "echo \"Error: no test specified\" && exit 1"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
...@@ -19,6 +18,8 @@ ...@@ -19,6 +18,8 @@
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.21.1", "express": "^4.21.1",
"express-session": "^1.18.1", "express-session": "^1.18.1",
"jest": "^29.7.0",
"joi": "^17.13.3",
"mongoose": "^8.8.1", "mongoose": "^8.8.1",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
...@@ -27,10 +28,12 @@ ...@@ -27,10 +28,12 @@
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"sequelize": "^6.37.5", "sequelize": "^6.37.5",
"sequelize-cli": "^6.6.2" "sequelize-cli": "^6.6.2",
"sqlite3": "^5.1.7"
}, },
"devDependencies": { "devDependencies": {
"artillery": "^2.0.21", "artillery": "^2.0.21",
"cross-env": "^7.0.3",
"nodemon": "^3.1.7" "nodemon": "^3.1.7"
} }
} }
...@@ -6,7 +6,7 @@ const User = require('../models/User'); ...@@ -6,7 +6,7 @@ const User = require('../models/User');
const sequelize = require('../config/sequelize'); const sequelize = require('../config/sequelize');
// DTO 임포트 // DTO 임포트
const FriendRequestDTO = require('../dtos/FriendRequestDTO'); const FriendResponseDTO = require('../dtos/FriendResponseDTO');
const FriendListDTO = require('../dtos/FriendListDTO'); const FriendListDTO = require('../dtos/FriendListDTO');
class FriendService { class FriendService {
...@@ -28,7 +28,7 @@ class FriendService { ...@@ -28,7 +28,7 @@ class FriendService {
* 친구 요청 보내기 * 친구 요청 보내기
* @param {number} userId - 친구 요청을 보내는 사용자 ID * @param {number} userId - 친구 요청을 보내는 사용자 ID
* @param {number} friendId - 친구 요청을 받는 사용자 ID * @param {number} friendId - 친구 요청을 받는 사용자 ID
* @returns {Promise<FriendRequestDTO>} - 생성된 친구 요청 DTO * @returns {Promise<FriendResponseDTO>} - 생성된 친구 요청 DTO
* @throws {Error} - 유효하지 않은 요청일 경우 * @throws {Error} - 유효하지 않은 요청일 경우
*/ */
async sendFriendRequest(userId, friendId) { async sendFriendRequest(userId, friendId) {
...@@ -71,7 +71,7 @@ class FriendService { ...@@ -71,7 +71,7 @@ class FriendService {
return new FriendRequestDTO(friendRequestWithDetails.toJSON()); return new FriendResponseDTO(friendRequestWithDetails.toJSON());
} catch (error) { } catch (error) {
if (error.name === 'SequelizeUniqueConstraintError') { if (error.name === 'SequelizeUniqueConstraintError') {
throw new Error('Friend request already exists'); throw new Error('Friend request already exists');
...@@ -83,7 +83,7 @@ class FriendService { ...@@ -83,7 +83,7 @@ class FriendService {
/** /**
* 받은 친구 요청 목록 조회 * 받은 친구 요청 목록 조회
* @param {number} userId - 요청을 받은 사용자 ID * @param {number} userId - 요청을 받은 사용자 ID
* @returns {Promise<Array<FriendRequestDTO>>} - 받은 친구 요청 목록 DTO 배열 * @returns {Promise<Array<FriendResponseDTO>>} - 받은 친구 요청 목록 DTO 배열
*/ */
async getReceivedRequests(userId) { async getReceivedRequests(userId) {
const receivedRequests = await Friend.findAll({ const receivedRequests = await Friend.findAll({
...@@ -97,13 +97,13 @@ class FriendService { ...@@ -97,13 +97,13 @@ class FriendService {
] ]
}); });
return receivedRequests.map(req => new FriendRequestDTO(req)); return receivedRequests.map(req => new FriendResponseDTO(req));
} }
/** /**
* 보낸 친구 요청 목록 조회 * 보낸 친구 요청 목록 조회
* @param {number} userId - 요청을 보낸 사용자 ID * @param {number} userId - 요청을 보낸 사용자 ID
* @returns {Promise<Array<FriendRequestDTO>>} - 보낸 친구 요청 목록 DTO 배열 * @returns {Promise<Array<FriendResponseDTO>>} - 보낸 친구 요청 목록 DTO 배열
*/ */
async getSentRequests(userId) { async getSentRequests(userId) {
const sentRequests = await Friend.findAll({ const sentRequests = await Friend.findAll({
...@@ -117,14 +117,14 @@ class FriendService { ...@@ -117,14 +117,14 @@ class FriendService {
] ]
}); });
return sentRequests.map(req => new FriendRequestDTO(req)); return sentRequests.map(req => new FriendResponseDTO(req));
} }
/** /**
* 친구 요청 수락 * 친구 요청 수락
* @param {number} userId - 요청을 수락하는 사용자 ID * @param {number} userId - 요청을 수락하는 사용자 ID
* @param {number} friendId - 친구 요청을 보낸 사용자 ID * @param {number} friendId - 친구 요청을 보낸 사용자 ID
* @returns {Promise<FriendRequestDTO>} - 업데이트된 친구 요청 DTO * @returns {Promise<FriendResponseDTO>} - 업데이트된 친구 요청 DTO
* @throws {Error} - 친구 요청이 존재하지 않을 경우 * @throws {Error} - 친구 요청이 존재하지 않을 경우
*/ */
async acceptFriendRequest(userId, friendId) { async acceptFriendRequest(userId, friendId) {
...@@ -155,7 +155,7 @@ class FriendService { ...@@ -155,7 +155,7 @@ class FriendService {
] ]
}); });
return new FriendRequestDTO(updatedRequest); return new FriendResponseDTO(updatedRequest);
} catch (error) { } catch (error) {
await transaction.rollback(); await transaction.rollback();
throw error; throw error;
......
// 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');
});
});
});
// 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('../services/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).toBe(true);
// 삭제된 스케줄이 실제로 삭제되었는지 확인
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');
});
});
describe('cleanExpiredSchedules', () => {
test('should delete expired flexible schedules', async () => {
// 만료된 유동 스케줄 생성
await Schedule.create({
id: 3,
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-04-30T00:00:00Z'), // 이미 만료됨
});
// 만료되지 않은 유동 스케줄 생성
await Schedule.create({
id: 4,
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.findByPk(3);
expect(expiredSchedule).toBeNull();
// 만료되지 않은 스케줄은 남아있는지 확인
const validSchedule = await Schedule.findByPk(4);
expect(validSchedule).toBeDefined();
expect(validSchedule.title).toBe('Valid Flexible Schedule');
});
});
});
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